@nktkas/hyperliquid
Version:
Hyperliquid API SDK for all major JS runtimes, written in TypeScript.
326 lines (296 loc) • 9.81 kB
text/typescript
// ============================================================
// Extract Args
// ============================================================
export type RawArgs<
Collect extends boolean = true,
DoubleDash extends boolean = false,
> =
& { _: string[] }
& (DoubleDash extends true ? { "--": string[] } : { "--"?: never })
& Record<string, Collect extends false ? string : string | string[]>;
export type ExtractOptions<
Collect extends boolean = true,
DoubleDash extends boolean = false,
> = {
/**
* Consider these keys as flags (no value expected).
* Value going after a flag will be treated as a positional argument.
*/
flags?: string[];
/**
* Collect repeated options into arrays.
* - `true` - repeated keys accumulate values into an array.
* - `false` - later values overwrite earlier ones.
* @default true
*/
collect?: Collect;
/**
* Handle `--no-*` as negation (sets value to "false").
* - `true` - `--no-foo` becomes `{ foo: "false" }`.
* - `false` - `--no-foo` becomes `{ "no-foo": "true" }`.
* @default true
*/
negatable?: boolean;
/**
* Store arguments after `--` in a separate array.
* - `true` - arguments after `--` are stored in `"--"` array.
* - `false` - arguments after `--` are added to `_` array.
* @default false
*/
doubleDash?: DoubleDash;
};
/** Check if next argument can be used as a value. */
function isValidValue(nextArg: string | undefined): nextArg is string {
if (typeof nextArg !== "string") return false;
// Allow negative numbers as values (e.g., -5, -3.14)
if (nextArg.startsWith("-") && isNaN(Number(nextArg))) return false;
return true;
}
/** Set value for a key, with optional array collection on repeat. */
function setValue(
result: Record<string, string | string[]>,
key: string,
value: string,
collect: boolean,
): void {
if (key in result && collect) {
// Key already exists and collect is enabled - convert to array or push
const existing = result[key];
if (Array.isArray(existing)) {
existing.push(value); // Already an array - just push
} else {
result[key] = [existing, value]; // First repeat - convert to array
}
} else {
// First occurrence or collect disabled - set/overwrite value
result[key] = value;
}
}
/** Extract command-line arguments into a key-value object (does not transform values). */
export function extractArgs<
Collect extends boolean = true,
DoubleDash extends boolean = false,
>(
args: string[],
options?: ExtractOptions<Collect, DoubleDash>,
): RawArgs<Collect, DoubleDash> {
const flags = options?.flags ?? [];
const collect = options?.collect ?? true;
const negatable = options?.negatable ?? true;
const doubleDash = options?.doubleDash ?? false;
const result = (doubleDash ? { _: [], "--": [] } : { _: [] }) as unknown as RawArgs<Collect, DoubleDash>;
let terminated = false; // After "--", all args are positional
for (let i = 0; i < args.length; i++) {
const arg = args[i];
// After "--" terminator, all arguments are positional
if (terminated) {
if (doubleDash) {
(result as RawArgs<Collect, true>)["--"].push(arg);
} else {
result._.push(arg);
}
continue;
}
// Options terminator: "--"
if (arg === "--") {
terminated = true;
continue;
}
// Positional argument (doesn't start with "-")
if (!arg.startsWith("-")) {
result._.push(arg);
continue;
}
const isLongOption = arg.startsWith("--");
// Handle long options (--key, --key=value, --no-key)
if (isLongOption) {
const rawKey = arg.slice(2);
const equalsIndex = rawKey.indexOf("=");
const hasInlineValue = equalsIndex !== -1;
let key = hasInlineValue ? rawKey.substring(0, equalsIndex) : rawKey;
// Skip reserved keys "_" and "--"
if (key === "_" || key === "--") continue;
let value: string;
if (negatable && key.startsWith("no-") && !hasInlineValue) { // Handle --no-* negation
key = key.slice(3); // Remove "no-" prefix
value = "false";
} else if (hasInlineValue) { // Handle --key=value
value = rawKey.substring(equalsIndex + 1);
} else if (!flags.includes(key) && isValidValue(args[i + 1])) { // Handle --key value
value = args[++i];
} else { // Flag without value
value = "true";
}
setValue(result, key, value, collect);
} // Handle short options (-v, -f value, -abc)
else {
const chars = arg.slice(1);
// Single short option: -f
if (chars.length === 1) {
const key = chars;
if (key === "_") continue; // Skip reserved key
let value: string;
if (!flags.includes(key) && isValidValue(args[i + 1])) { // Handle -f value
value = args[++i];
} else { // Flag without value
value = "true";
}
setValue(result, key, value, collect);
} // Grouped short flags: -abc → a, b, c (all flags, no values)
else {
for (const char of chars) {
if (char === "_") continue; // Skip reserved key
setValue(result, char, "true", collect);
}
}
}
}
return result;
}
// ============================================================
// Transform Args
// ============================================================
export interface TransformOptions {
/**
* Transform rule for null values.
* @value `"null"` (case insensitive)
* @default null
*/
null?: "null" | "string";
/**
* Transform rule for boolean values.
* @value `"true"` / `"false"` (case insensitive)
* @default bool
*/
bool?: "bool" | "string";
/**
* Transform rule for hexadecimal values.
* @value `"0xff"` (case insensitive) (0x prefix required)
* @default "string"
*/
hex?: "string" | "number";
/**
* Transform rule for special numeric values.
* @value `"Infinity"` / `"NaN"` (case insensitive) (may include leading sign)
* @default "number"
*/
specialNumber?: "string" | "number";
/**
* Transform rule for numeric values.
* @value `"123"` / `"12.3"` / `"1e+2"` / `"-123"` (anything that can be parsed by `Number(value)`; excludes hex values)
* @default "number"
*/
number?: "number" | "string";
/**
* Transform rule for JSON object/array values.
* @value `"{ \"a\": 1 }"`, `"[invalid array string]"` (anything that begins with `{` and ends with `}` or begins with `[` and ends with `]`)
* @default "object"
*/
json?: "object" | "string";
}
type TransformedValue = string | number | boolean | null | Record<string, unknown> | unknown[];
export type Args<
Collect extends boolean = true,
DoubleDash extends boolean = false,
> =
& { _: string[] }
& (DoubleDash extends true ? { "--": string[] } : { "--"?: never })
& Record<string, Collect extends false ? TransformedValue : TransformedValue | TransformedValue[]>;
/** Transform a single string value based on rules. */
function transformValue(
value: string,
opts: {
null: "null" | "string";
bool: "bool" | "string";
hex: "string" | "number";
specialNumber: "string" | "number";
number: "number" | "string";
json: "object" | "string";
},
): Args[string] {
// null
if (value.toLowerCase() === "null") {
return opts.null === "null" ? null : value;
}
// boolean
if (value.toLowerCase() === "true" || value.toLowerCase() === "false") {
return opts.bool === "bool" ? value.toLowerCase() === "true" : value;
}
// hex
if (
(value.startsWith("0x") || value.startsWith("0X")) &&
!isNaN(Number(value))
) {
return opts.hex === "string" ? value : Number(value);
}
// special number
if (
value.toLowerCase() === "infinity" ||
value.toLowerCase() === "-infinity" ||
value.toLowerCase() === "nan" ||
value.toLowerCase() === "-nan"
) {
return opts.specialNumber === "string" ? value : Number(value);
}
// number
if (!isNaN(Number(value))) {
return opts.number === "number" ? Number(value) : value;
}
// json object/array
if (
(value.startsWith("{") && value.endsWith("}")) ||
(value.startsWith("[") && value.endsWith("]"))
) {
if (opts.json === "object") {
try {
return JSON.parse(value);
} catch { /** Ignore invalid JSON */ }
}
return value; // as string if parsing fails or opts is "string"
}
// string (default)
return value;
}
/**
* Transform raw args (string values) into typed args based on rules.
*
* Order of transformation:
* 1. null (`"null"`)
* 2. boolean (`"true"`, `"false"`)
* 3. hex (`"0x..."`)
* 4. special number (`"Infinity"`, `"-Infinity"`, `"NaN", "-NaN"`)
* 5. number (numeric strings)
* 6. json (object/array strings)
* 7. string (default)
*/
export function transformArgs<
Collect extends boolean = true,
DoubleDash extends boolean = false,
>(
args: RawArgs<Collect, DoubleDash>,
options?: TransformOptions,
): Args<Collect, DoubleDash> {
const opts = {
null: options?.null ?? "null",
bool: options?.bool ?? "bool",
hex: options?.hex ?? "string",
specialNumber: options?.specialNumber ?? "number",
number: options?.number ?? "number",
json: options?.json ?? "object",
} as const;
const result = ("--" in args ? { _: args._, "--": args["--"] } : { _: args._ }) as unknown as Args<
Collect,
DoubleDash
>;
for (const key in args) {
if (key === "_" || key === "--") continue; // Skip positional args arrays
const value = args[key];
// Handle arrays (repeated options)
if (Array.isArray(value)) {
result[key] = value.map((v) => transformValue(v, opts));
} else {
result[key] = transformValue(value, opts);
}
}
return result;
}