Skip to content

Commit

Permalink
fix: args typed with undefined (#389)
Browse files Browse the repository at this point in the history
* test: initialize type tests

* test: expand type tests

* test: add failing test

* test: add failing tests

* fix: types

* build: fix cc

* build: cache node_modules

* build: cache more

* build: fix artifacts (?)

* build: fix artifact path (?)
  • Loading branch information
hongaar authored Mar 22, 2022
1 parent 0e72d22 commit 419f048
Show file tree
Hide file tree
Showing 17 changed files with 756 additions and 82 deletions.
26 changes: 25 additions & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,40 @@ jobs:
- uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node }}
- id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn config get cacheFolder)"
- uses: actions/cache@v3
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- run: yarn install
- run: yarn test
- run: yarn build
- run: yarn test
- uses: actions/upload-artifact@v3
with:
name: lib
path: lib

coverage:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn config get cacheFolder)"
- uses: actions/cache@v3
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- run: yarn install
- uses: actions/download-artifact@v3
with:
name: lib
path: lib
- uses: paambaati/[email protected]
env:
CC_TEST_REPORTER_ID: e1a2f8ecd90c13810c302d9cdfb4a26a5b79666e899c4f353e558416c168da0d
Expand Down
9 changes: 7 additions & 2 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,14 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn config get cacheFolder)"
- uses: actions/cache@v3
with:
node-version: 14
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- run: yarn install
- run: npx semantic-release
env:
Expand Down
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"typescript.tsdk": "node_modules\\typescript\\lib"
"typescript.tsdk": "node_modules/typescript/lib"
}
4 changes: 1 addition & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -737,9 +737,7 @@ Optionally deploy to GitHub, S3, etc. using your preferred CD method if needed.

## Todo

- [ ] Better code coverage
- [ ] Consider resolving ambiguity in _prompt_ param/method
- [ ] Async autocomplete method for arg values
See [TODO.md](TODO.md)

## Contributing

Expand Down
12 changes: 8 additions & 4 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
### TODOs
| Filename | line # | TODO
|:------|:------:|:------
| [src/prompter.ts](src/prompter.ts#L16) | 16 | Wait for upstream change to import types from enquirer
| [src/prompter.ts](src/prompter.ts#L77) | 77 | ignoring type error here, probably need another type
| Filename | line # | TODO |
|:------|:------:|:------|
| [src/baseArg.ts](src/baseArg.ts#L88) | 88 | See if we can add this to autocompleter |
| [src/command.ts](src/command.ts#L278) | 278 | coerce all types and remove coerce option from baseArg |
| [src/command.ts](src/command.ts#L293) | 293 | Upgrade to native async handlers in yarn 17 |
| [src/prompter.ts](src/prompter.ts#L16) | 16 | Wait for upstream change to import types from enquirer |
| [src/prompter.ts](src/prompter.ts#L77) | 77 | ignoring type error here, probably need another type |
| [test-d/command.test-d.ts](test-d/command.test-d.ts#L89) | 89 | option types |
2 changes: 1 addition & 1 deletion examples/cat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { command, program } from '../src'

const cat = command('cat')
.description('Concatenate files')
.argument('files', { variadic: true })
.argument('files', { variadic: true, default: [] })
.action(({ files }) =>
console.log(
files.reduce((str, file) => str + readFileSync(file, 'utf8'), '')
Expand Down
57 changes: 54 additions & 3 deletions examples/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { program, command } from '../src'
import { command, program } from '../src'

/**
* Keep in mind that argument/option types are not validated at runtime.
Expand All @@ -7,8 +7,57 @@ import { program, command } from '../src'
*/

const string = command('string')
.argument('arg', { description: 'Required string argument' })
.option('opt', { description: 'Optional string option', type: 'string' })
.argument('arg1', { description: 'Required string argument' })
.argument('arg2', { optional: true, description: 'Optional string argument' })
.argument('arg3', { variadic: true, description: 'Variadic string argument' })
.option('opt1', { type: 'string', description: 'String option' })
.option('opt2', {
default: 'foo',
description: 'String option with default',
})
.action((args) => {
console.log('Args are', args)
})

const number = command('number')
.argument('arg1', { type: 'number', description: 'Required number argument' })
.argument('arg2', {
type: 'number',
optional: true,
description: 'Optional number argument',
})
.argument('arg3', {
type: 'number',
variadic: true,
description: 'Variadic number argument',
})
.option('opt1', { type: 'number', description: 'number option' })
.option('opt2', { default: 100, description: 'Number option with default' })
.action((args) => {
console.log('Args are', args)
})

const boolean = command('boolean')
.argument('arg1', {
type: 'boolean',
description: 'Required boolean argument',
})
.argument('arg2', {
type: 'boolean',
optional: true,
description: 'Optional boolean argument',
})
.argument('arg3', {
type: 'boolean',
variadic: true,
description: 'Variadic boolean argument',
})
.option('opt1', { type: 'boolean', description: 'number option' })
.option('opt2', {
type: 'boolean',
default: false,
description: 'Number option with default',
})
.action((args) => {
console.log('Args are', args)
})
Expand Down Expand Up @@ -41,6 +90,8 @@ const defaultValues = command('default')
const app = program()
.description('All argument and option types')
.add(string)
.add(number)
.add(boolean)
.add(choices)
.add(defaultValues)

Expand Down
10 changes: 6 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,16 @@
"files": [
"lib"
],
"types": "lib/index.d.ts",
"scripts": {
"prepublishOnly": "yarn build",
"build": "tsc",
"build": "tsc --project tsconfig.build.json",
"watch": "tsc --watch",
"lint": "prettier --write \"src/**/*\"",
"test": "jest",
"test": "tsd && jest",
"start": "ts-node",
"doc:toc": "doctoc README.md",
"doc:todos": "leasot --exit-nicely --reporter markdown \"src/**/*.ts\" > TODO.md"
"doc:todos": "leasot --exit-nicely --reporter markdown \"{src,test-d,tests}/**/*.ts\" > TODO.md"
},
"author": "",
"license": "MIT",
Expand All @@ -57,6 +58,7 @@
"prettier": "2.6.0",
"ts-jest": "27.1.3",
"ts-node": "10.7.0",
"tsd": "^0.19.1",
"typescript": "4.6.2"
}
}
}
70 changes: 34 additions & 36 deletions src/baseArg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,43 +6,40 @@ export interface BaseArgOptions {
prompt?: true | string
}

// prettier-ignore
export type InferArgType<O extends Options | PositionalOptions, F = unknown> =
/**
* Add support for numeric variadic arguments
*/
O extends {
variadic: true
type: 'number'
}
? Array<number>
: /**
* Add support for string variadic arguments
*/
O extends {
variadic: true
}
? Array<string>
: /**
* Choices with array type
*/ O extends { choices: ReadonlyArray<infer C>; type: 'array' }
? C[]
: /**
* Choices with array default
*/ O extends {
choices: ReadonlyArray<infer C>
default: ReadonlyArray<string>
}
? C[]
: /**
* Prefer choices over default
*/ O extends { choices: ReadonlyArray<infer C> }
? C
: /**
* Allow fallback type
*/
unknown extends InferredOptionType<O>
? F
: InferredOptionType<O>
// Default number
O extends { default: number } ? number :
// Optional number
O extends { type: 'number', optional: true } ? number | undefined :
// Variadic number
O extends { type: 'number', variadic: true } ? Array<number> :
// Number
O extends { type: 'number' } ? number :
// Default boolean
O extends { default: boolean } ? boolean :
// Optional boolean
O extends { type: 'boolean', optional: true } ? boolean | undefined :
// Variadic boolean
O extends { type: 'boolean', variadic: true } ? Array<boolean> :
// Boolean
O extends { type: 'boolean' } ? boolean :
// Default string
O extends { default: string } ? string :
// Optional string
O extends { optional: true } ? string | undefined :
// Variadic string
O extends { variadic: true } ? Array<string> :
// Choices with array type
O extends { choices: ReadonlyArray<infer C>; type: 'array' } ? C[] :
// Choices with array default
O extends { choices: ReadonlyArray<infer C>, default: ReadonlyArray<string> } ? C[] :
// Prefer choices over default
O extends { choices: ReadonlyArray<infer C> } ? C :
// Allow fallback type
unknown extends InferredOptionType<O> ? F :
// yargs type
InferredOptionType<O>

export class BaseArg {
protected name: string
Expand Down Expand Up @@ -87,6 +84,7 @@ export class BaseArg {

/**
* Get possible values, is specified.
* @todo See if we can add this to autocompleter
*/
getChoices() {
return this.options.choices
Expand Down
6 changes: 4 additions & 2 deletions src/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export class Command<T = {}> {
) {
this.add(new Argument(name, options))

return (this as unknown) as Command<
return this as unknown as Command<
T & { [key in K]: InferArgType<O, string> }
>
}
Expand All @@ -107,7 +107,7 @@ export class Command<T = {}> {
) {
this.add(new Option(name, options))

return (this as unknown) as Command<T & { [key in K]: InferArgType<O> }>
return this as unknown as Command<T & { [key in K]: InferArgType<O> }>
}

/**
Expand Down Expand Up @@ -275,6 +275,7 @@ export class Command<T = {}> {
let promise = prompterInstance.prompt()

promise = promise.then((args) => {
// @todo coerce all types and remove coerce option from baseArg
if (this.handler) {
return this.handler(args, commandRunner)
}
Expand All @@ -289,6 +290,7 @@ export class Command<T = {}> {

// Save promise chain on argv instance, so we can access it in parse
// callback.
// @todo Upgrade to native async handlers in yarn 17
argv.__promise = promise

return promise
Expand Down
7 changes: 4 additions & 3 deletions src/option.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Options as BaseOptions, Argv } from 'yargs'
import { BaseArgOptions, BaseArg } from './baseArg'
import { Argv, Options as BaseOptions } from 'yargs'
import { BaseArg, BaseArgOptions } from './baseArg'

// We ignore some not-so-common use cases from the type to make using this
// library easier. They could still be used at runtime but won't be documented
Expand All @@ -14,6 +14,7 @@ type IgnoreOptions =
| 'defaultDescription'
| 'demand'
| 'demandOption'
| 'deprecate'
| 'desc'
| 'describe'
| 'global'
Expand All @@ -28,7 +29,7 @@ type IgnoreOptions =
| 'requiresArg'
| 'skipValidation'
| 'string'
// | 'implies'
| 'implies'

export interface OptionOptions
extends Omit<BaseOptions, IgnoreOptions>,
Expand Down
Loading

0 comments on commit 419f048

Please sign in to comment.