Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: isaacs/jackspeak
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v3.2.3
Choose a base ref
...
head repository: isaacs/jackspeak
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: v3.2.4
Choose a head ref
  • 2 commits
  • 5 files changed
  • 1 contributor

Commits on Jun 4, 2024

  1. Infer 'validOptions' all the way through, when possible

    Also, format the test file.
    isaacs committed Jun 4, 2024

    Verified

    This commit was signed with the committer’s verified signature. The key has expired.
    isaacs isaacs
    Copy the full SHA
    b803231 View commit details
  2. 3.2.4

    isaacs committed Jun 4, 2024

    Verified

    This commit was signed with the committer’s verified signature. The key has expired.
    isaacs isaacs
    Copy the full SHA
    3fe68cd View commit details
Showing with 102 additions and 69 deletions.
  1. +17 −11 README.md
  2. +2 −2 package-lock.json
  3. +1 −1 package.json
  4. +34 −32 src/index.ts
  5. +48 −23 test/basic.ts
28 changes: 17 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
@@ -35,16 +35,16 @@ import { jack } from 'jackspeak'

const { positionals, values } = jack({ envPrefix: 'FOO' })
.flag({
asdf: { description: 'sets the asfd flag', short: 'a', default: true },
'no-asdf': { description: 'unsets the asdf flag', short: 'A' },
foo: { description: 'another boolean', short: 'f' },
asdf: { description: 'sets the asfd flag', short: 'a', default: true } as const,
'no-asdf': { description: 'unsets the asdf flag', short: 'A' } as const,
foo: { description: 'another boolean', short: 'f' } as const,
})
.optList({
'ip-addrs': {
description: 'addresses to ip things',
delim: ',', // defaults to '\n'
default: ['127.0.0.1'],
},
} as const,
})
.parse([
'some',
@@ -138,6 +138,12 @@ Configs are defined by calling the appropriate field definition
method with an object where the keys are the long option name,
and the value defines the config.
It's best to define the option definitions `as const`, so that
`validOptions` all type information be inferred fully through to
the resulting parsed values. (It'll do its best with what it's
given otherwise, but you won't get the `validOptions` showing up
as the only possible values according to TS.)
Options:
- `type` Only needed for the `addFields` method, as the others
@@ -161,7 +167,7 @@ Options:
- `default` A default value for the field. Note that this may be
overridden by an environment variable, if present.

### `Jack.flag({ [option: string]: definition, ... })`
### `Jack.flag({ [option: string]: definition as const, ... })`

Define one or more boolean fields.

@@ -173,31 +179,31 @@ If a boolean option named `no-${optionName}` with the same
`multiple` setting is in the configuration, then that will be
treated as a negating flag.
### `Jack.flagList({ [option: string]: definition, ... })`
### `Jack.flagList({ [option: string]: definition as const, ... })`
Define one or more boolean array fields.
### `Jack.num({ [option: string]: definition, ... })`
### `Jack.num({ [option: string]: definition as const, ... })`
Define one or more number fields. These will be set in the
environment as a stringified number, and included in the `values`
object as a number.
### `Jack.numList({ [option: string]: definition, ... })`
### `Jack.numList({ [option: string]: definition as const, ... })`
Define one or more number list fields. These will be set in the
environment as a delimited set of stringified numbers, and
included in the `values` as a number array.
### `Jack.opt({ [option: string]: definition, ... })`
### `Jack.opt({ [option: string]: definition as const, ... })`
Define one or more string option fields.
### `Jack.optList({ [option: string]: definition, ... })`
### `Jack.optList({ [option: string]: definition as const, ... })`
Define one or more string list fields.
### `Jack.addFields({ [option: string]: definition, ... })`
### `Jack.addFields({ [option: string]: definition as const, ... })`
Define one or more fields of any type. Note that `type` and
`multiple` must be set explicitly on each definition when using
4 changes: 2 additions & 2 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
@@ -1,6 +1,6 @@
{
"name": "jackspeak",
"version": "3.2.3",
"version": "3.2.4",
"description": "A very strict and proper argument parser.",
"tshy": {
"main": true,
66 changes: 34 additions & 32 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -13,6 +13,13 @@ import { parseArgs } from './parse-args.js'
import cliui from '@isaacs/cliui'
import { basename } from 'node:path'

export type ValidOptions<T extends ConfigType> =
| undefined
| (T extends 'boolean' ? never
: T extends 'string' ? readonly string[]
: T extends 'number' ? readonly number[]
: readonly number[] | readonly string[])

const width = Math.min(
(process && process.stdout && process.stdout.columns) || 80,
80,
@@ -92,17 +99,7 @@ export type ValidValue<
export type ConfigOptionMeta<
T extends ConfigType,
M extends boolean = boolean,
O extends
| undefined
| (T extends 'boolean' ? never
: T extends 'string' ? readonly string[]
: T extends 'number' ? readonly number[]
: readonly number[] | readonly string[]) =
| undefined
| (T extends 'boolean' ? never
: T extends 'string' ? readonly string[]
: T extends 'number' ? readonly number[]
: readonly number[] | readonly string[]),
O extends ValidOptions<T> = ValidOptions<T>
> = {
default?:
| undefined
@@ -144,7 +141,11 @@ export type ConfigSetFromMetaSet<
M extends boolean,
S extends ConfigMetaSet<T, M>,
> = {
[longOption in keyof S]: ConfigOptionBase<T, M>
[longOption in keyof S]: ConfigOptionBase<
T,
M,
S[longOption]['validOptions']
>
}

/**
@@ -174,18 +175,17 @@ export type MultiType<M extends boolean> =
export type ConfigOptionBase<
T extends ConfigType,
M extends boolean = boolean,
O extends ValidOptions<T> = ValidOptions<T>,
> = {
type: T
short?: string | undefined
default?: ValidValue<T, M> | undefined
description?: string
hint?: T extends 'boolean' ? undefined : string | undefined
validate?: (v: unknown) => v is ValidValue<T, M>
validOptions?: T extends 'boolean' ? undefined
: T extends 'string' ? readonly string[]
: T extends 'number' ? readonly number[]
: readonly number[] | readonly string[]
} & MultiType<M>
} & (
O extends undefined ? { validOptions?: O } : { validOptions: O }
)& MultiType<M>

export const isConfigType = (t: string): t is ConfigType =>
typeof t === 'string' &&
@@ -239,7 +239,7 @@ export const isConfigOption = <T extends ConfigType, M extends boolean>(
o: any,
type: T,
multi: M,
): o is ConfigOptionBase<T, M> =>
): o is ConfigOptionBase<T, M, (typeof o)['validOptions']> =>
!!o &&
typeof o === 'object' &&
isConfigType(o.type) &&
@@ -269,9 +269,13 @@ export type OptionsResults<T extends ConfigSet> = {
[k in keyof T]?: T[k]['validOptions'] extends (
readonly string[] | readonly number[]
) ?
T[k] extends ConfigOptionBase<'string' | 'number', false> ?
T[k] extends (
ConfigOptionBase<'string' | 'number', false, T[k]['validOptions']>
) ?
T[k]['validOptions'][number]
: T[k] extends ConfigOptionBase<'string' | 'number', true> ?
: T[k] extends (
ConfigOptionBase<'string' | 'number', true, T[k]['validOptions']>
) ?
T[k]['validOptions'][number][]
: never
: T[k] extends ConfigOptionBase<'string', false> ? string
@@ -293,7 +297,7 @@ export type Parsed<T extends ConfigSet> = {

function num(
o: ConfigOptionMeta<'number', false> = {},
): ConfigOptionBase<'number', false> {
): ConfigOptionBase<'number', false, (typeof o)['validOptions']> {
const { default: def, validate: val, validOptions, ...rest } = o
if (def !== undefined && !isValidValue(def, 'number', false)) {
throw new TypeError('invalid default value', {
@@ -327,7 +331,7 @@ function num(

function numList(
o: ConfigOptionMeta<'number'> = {},
): ConfigOptionBase<'number', true> {
): ConfigOptionBase<'number', true, (typeof o)['validOptions']> {
const { default: def, validate: val, validOptions, ...rest } = o
if (def !== undefined && !isValidValue(def, 'number', true)) {
throw new TypeError('invalid default value', {
@@ -361,7 +365,7 @@ function numList(

function opt(
o: ConfigOptionMeta<'string', false> = {},
): ConfigOptionBase<'string', false> {
): ConfigOptionBase<'string', false, (typeof o)['validOptions']> {
const { default: def, validate: val, validOptions, ...rest } = o
if (def !== undefined && !isValidValue(def, 'string', false)) {
throw new TypeError('invalid default value', {
@@ -395,7 +399,7 @@ function opt(

function optList(
o: ConfigOptionMeta<'string'> = {},
): ConfigOptionBase<'string', true> {
): ConfigOptionBase<'string', true, (typeof o)['validOptions']> {
const { default: def, validate: val, validOptions, ...rest } = o
if (def !== undefined && !isValidValue(def, 'string', true)) {
throw new TypeError('invalid default value', {
@@ -429,7 +433,7 @@ function optList(

function flag(
o: ConfigOptionMeta<'boolean', false> = {},
): ConfigOptionBase<'boolean', false> {
): ConfigOptionBase<'boolean', false, undefined> {
const {
hint,
default: def,
@@ -458,7 +462,7 @@ function flag(

function flagList(
o: ConfigOptionMeta<'boolean'> = {},
): ConfigOptionBase<'boolean', true> {
): ConfigOptionBase<'boolean', true, undefined> {
const {
hint,
default: def,
@@ -1090,7 +1094,9 @@ export class Jack<C extends ConfigSet = {}> {
F extends ConfigMetaSet<T, M>,
>(
fields: F,
fn: (m: ConfigOptionMeta<T, M>) => ConfigOptionBase<T, M>,
fn: (
m: ConfigOptionMeta<T, M, ValidOptions<T>>,
) => ConfigOptionBase<T, M>,
): Jack<C & ConfigSetFromMetaSet<T, M, F>> {
type NextC = C & ConfigSetFromMetaSet<T, M, F>
const next = this as unknown as Jack<NextC>
@@ -1438,11 +1444,7 @@ const normalize = (s: string, pre = false) => {
const i = isFinite(si) ? si : 0
return (
'\n```\n' +
split.map(
s =>
`\u200b${s.substring(i)}`,
)
.join('\n') +
split.map(s => `\u200b${s.substring(i)}`).join('\n') +
'\n```\n'
)
}
71 changes: 48 additions & 23 deletions test/basic.ts
Original file line number Diff line number Diff line change
@@ -493,7 +493,7 @@ t.test('parseRaw', t => {
xyz: {
default: 345,
validate: (n: unknown) => Number(n) % 2 === 1,
}
},
})
const p = j.parseRaw(['--xyz=235'])
t.equal(p.values.xyz, 235)
@@ -505,35 +505,60 @@ t.test('parseRaw', t => {
})

t.test('description with fenced code blocks', t => {
const j = jack({})
.num({
xyz: {
description: `Sometimes, there's a number and you care about
doing something special with that number, like
const j = jack({}).num({
xyz: {
description: `Sometimes, there's a number and you care about
doing something special with that number, like
\`\`\`
console.log(
heloo, number
\`\`\`
console.log(
heloo, number
such a fine day we're having,isn't it?
such a fine day we're having,isn't it?
Ok, goodbye then.
)
\`\`\`
Ok, goodbye then.
)
\`\`\`
this is some stuff that happens later
this is some stuff that happens later
\`\`\`
just one line, no indentation
\`\`\`
\`\`\`
just one line, no indentation
\`\`\`
nothing in this one:
\`\`\`
\`\`\`
`
},
})
nothing in this one:
\`\`\`
\`\`\`
`,
},
})
t.matchSnapshot(j.usage())
t.end()
})

t.test('verify validOptions show up as values[key] type', t => {
const j = jack({})
.opt({
foo: {
validOptions: ['bar', 'baz'],
} as const,
})
.num({
n: { validOptions: [1, 2, 3] } as const,
})

const { values } = j.parse([])
values.foo = 'bar'
values.foo = 'baz'
//@ts-expect-error
values.foo = 'asdf'

values.n = 1
values.n = 2
values.n = 3
//@ts-expect-error
values.n = 0
t.pass('typechecks passed')
t.end()
})