## Summary When parsing cli args, 'mri' returns an array for duplicate args, which causes unexpected results. All current arguments assume single values only. Tailwind v3 works the same way. ## Test plan Updated tests. --------- Co-authored-by: Jordan Pittman <jordan@cryptica.me>
181 lines
4.3 KiB
TypeScript
181 lines
4.3 KiB
TypeScript
import parse from 'mri'
|
|
|
|
// Definition of the arguments for a command in the CLI.
|
|
export type Arg = {
|
|
[key: `--${string}`]: {
|
|
type: keyof Types
|
|
description: string
|
|
alias?: `-${string}`
|
|
default?: Types[keyof Types]
|
|
values?: string[]
|
|
}
|
|
}
|
|
|
|
// Each argument will have a type and we want to convert the incoming raw string
|
|
// based value to the correct type. We can't use pure TypeScript types because
|
|
// these don't exist at runtime. Instead, we define a string-based type that
|
|
// maps to a TypeScript type.
|
|
type Types = {
|
|
boolean: boolean
|
|
number: number | null
|
|
string: string | null
|
|
'boolean | string': boolean | string | null
|
|
'number | string': number | string | null
|
|
'boolean | number': boolean | number | null
|
|
'boolean | number | string': boolean | number | string | null
|
|
}
|
|
|
|
// Convert the `Arg` type to a type that can be used at runtime.
|
|
//
|
|
// E.g.:
|
|
//
|
|
// Arg:
|
|
// ```
|
|
// { '--input': { type: 'string', description: 'Input file', alias: '-i' } }
|
|
// ```
|
|
//
|
|
// Command:
|
|
// ```
|
|
// ./tailwindcss -i input.css
|
|
// ./tailwindcss --input input.css
|
|
// ```
|
|
//
|
|
// Result type:
|
|
// ```
|
|
// {
|
|
// _: string[], // All non-flag arguments
|
|
// '--input': string | null // The `--input` flag will be filled with `null`, if the flag is not used.
|
|
// // The `null` type will not be there if `default` is provided.
|
|
// }
|
|
// ```
|
|
//
|
|
// Result runtime object:
|
|
// ```
|
|
// {
|
|
// _: [],
|
|
// '--input': 'input.css'
|
|
// }
|
|
// ```
|
|
export type Result<T extends Arg> = {
|
|
[K in keyof T]: T[K] extends { type: keyof Types; default?: any }
|
|
? undefined extends T[K]['default']
|
|
? Types[T[K]['type']]
|
|
: NonNullable<Types[T[K]['type']]>
|
|
: never
|
|
} & {
|
|
// All non-flag arguments
|
|
_: string[]
|
|
}
|
|
|
|
export function args<const T extends Arg>(options: T, argv = process.argv.slice(2)): Result<T> {
|
|
for (let [idx, value] of argv.entries()) {
|
|
if (value === '-') {
|
|
argv[idx] = '__IO_DEFAULT_VALUE__'
|
|
}
|
|
}
|
|
|
|
let parsed = parse(argv)
|
|
|
|
for (let key in parsed) {
|
|
let value = parsed[key]
|
|
|
|
if (key !== '_' && Array.isArray(value)) {
|
|
value = value[value.length - 1]
|
|
}
|
|
|
|
if (value === '__IO_DEFAULT_VALUE__') {
|
|
value = '-'
|
|
}
|
|
|
|
parsed[key] = value
|
|
}
|
|
|
|
let result: { _: string[]; [key: string]: unknown } = {
|
|
_: parsed._,
|
|
}
|
|
|
|
for (let [
|
|
flag,
|
|
{ type, alias, default: defaultValue = type === 'boolean' ? false : null },
|
|
] of Object.entries(options)) {
|
|
// Start with the default value
|
|
result[flag] = defaultValue
|
|
|
|
// Try to find the `alias`, and map it to long form `flag`
|
|
if (alias) {
|
|
let key = alias.slice(1)
|
|
if (parsed[key] !== undefined) {
|
|
result[flag] = convert(parsed[key], type)
|
|
}
|
|
}
|
|
|
|
// Try to find the long form `flag`
|
|
{
|
|
let key = flag.slice(2)
|
|
if (parsed[key] !== undefined) {
|
|
result[flag] = convert(parsed[key], type)
|
|
}
|
|
}
|
|
}
|
|
|
|
return result as Result<T>
|
|
}
|
|
|
|
// ---
|
|
|
|
type ArgumentType = string | boolean
|
|
|
|
// Try to convert the raw incoming `value` (which will be a string or a boolean,
|
|
// this is coming from `mri`'s parse function'), to the correct type based on
|
|
// the `type` of the argument.
|
|
function convert<T extends keyof Types>(value: string | boolean, type: T) {
|
|
switch (type) {
|
|
case 'string':
|
|
return convertString(value)
|
|
case 'boolean':
|
|
return convertBoolean(value)
|
|
case 'number':
|
|
return convertNumber(value)
|
|
case 'boolean | string':
|
|
return convertBoolean(value) ?? convertString(value)
|
|
case 'number | string':
|
|
return convertNumber(value) ?? convertString(value)
|
|
case 'boolean | number':
|
|
return convertBoolean(value) ?? convertNumber(value)
|
|
case 'boolean | number | string':
|
|
return convertBoolean(value) ?? convertNumber(value) ?? convertString(value)
|
|
default:
|
|
throw new Error(`Unhandled type: ${type}`)
|
|
}
|
|
}
|
|
|
|
function convertBoolean(value: ArgumentType) {
|
|
if (value === true || value === false) {
|
|
return value
|
|
}
|
|
|
|
if (value === 'true') {
|
|
return true
|
|
}
|
|
|
|
if (value === 'false') {
|
|
return false
|
|
}
|
|
}
|
|
|
|
function convertNumber(value: ArgumentType) {
|
|
if (typeof value === 'number') {
|
|
return value
|
|
}
|
|
|
|
{
|
|
let valueAsNumber = Number(value)
|
|
if (!Number.isNaN(valueAsNumber)) {
|
|
return valueAsNumber
|
|
}
|
|
}
|
|
}
|
|
|
|
function convertString(value: ArgumentType) {
|
|
return `${value}`
|
|
}
|