8000 cli.toJSON() by mmkal · Pull Request #98 · mmkal/trpc-cli · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

cli.toJSON() #98

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Jun 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations 8000
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ trpc-cli transforms a [tRPC](https://trpc.io) router into a professional-grade C
- [effect](#effect)
- [tRPC v10 vs v11](#trpc-v10-vs-v11)
- [Output and lifecycle](#output-and-lifecycle)
- [`.toJSON()`](#tojson)
- [Testing your CLI](#testing-your-cli)
- [Features and Limitations](#features-and-limitations)
- [More Examples](#more-examples)
Expand Down Expand Up @@ -330,7 +331,7 @@ You can also explicitly opt into this behavior for any procedure by setting `jso
### API docs

<!-- codegen:start {preset: markdownFromJsdoc, source: src/index.ts, export: createCli} -->
#### [createCli](./src/index.ts#L125)
#### [createCli](./src/index.ts#L126)

Run a trpc router as a CLI.

Expand Down Expand Up @@ -709,6 +710,23 @@ cli.run({

You could also override `process.exit` to avoid killing the process at all - see [programmatic usage](#programmatic-usage) for an example.

## `.toJSON()`

If you want to generate a website/help docs for your CLI, you can use `.toJSON()`:

```ts
const myRouter = t.router({
hello: t.procedure
.input(z.object({firstName: z.string()}))
.mutation(({input}) => `hello, ${input.firstName}`),
})

const cli = createCli({router: myRouter, name: 'mycli', version: '1.2.3'})
cli.toJSON() // {"name":"mycli", "version": "1.2.3", "commands": [{"name"":"hello", "options": [{"name": "first-name", "required": true, ...}]}]}
```

This is a _rough_ JSON representation of the CLI - useful for generating documentation etc. It returns basic information about the CLI and each command - to get any extra details you will need to use the `cli.buildProgram()` method and walk the tree of commands yourself.

## Testing your CLI

Rather than testing your CLI via a subprocess, which is slow and doesn't provide great DX, it's better to use the router that is passed to it directly with [`createCallerFactory`](https://trpc.io/docs/server/server-side-calls#create-caller):
Expand Down
3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {inspect} from 'util'
import {JsonSchema7Type} from 'zod-to-json-schema'
import {addCompletions} from './completions'
import {FailedToExitError, CliValidationError} from './errors'
import {commandToJSON} from './json'
import {
flattenedProperties,
incompatiblePropertyPairs,
Expand Down Expand Up @@ -562,7 +563,7 @@ export function createCli<R extends AnyRouter>({router, ...params}: TrpcCliParam
})
}

return {run, buildProgram}
return {run, buildProgram, toJSON: (program = buildProgram()) => commandToJSON(program as Command)}
}

function getMeta(procedure: AnyProcedure): Omit<TrpcCliMeta, 'cliMeta'> {
Expand Down
96 changes: 96 additions & 0 deletions src/json.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import {Command} from 'commander'

/**
* JSON representation of a commander `Command` instance
* Note: this is not necessarily a _complete_ representation of the command - it aims to be a big enough subset to be useful for generating documentation etc.
*/
export type CommandJSON = {
name?: string
version?: string
description?: string
usage?: string
commands?: CommandJSON[]
arguments?: {
name: string
description?: string
required: boolean
defaultValue?: {}
defaultValueDescription?: string
variadic: boolean
choices?: string[]
}[]
options?: {
name: string
description?: string
required: boolean
defaultValue?: {}
defaultValueDescription?: string
variadic: boolean
attributeName?: string
flags?: string
short?: string
negate: boolean
optional: boolean
choices?: string[]
}[]
}

/**
* Convert a commander `Command` instance to a JSON object.
*
* Note: in theory you could use this with any `Command` instance, it doesn't have
* to be one built by `trpc-cli`. Implementing here because it's pretty simple to do and `commander` doesn't seem to provide a way to do it.
*
* Note: falsy values for strings are replaced with `undefined` in the output - e.g. if there's an empty description, it will be `undefined` in the output.
*/
export const commandToJSON = (command: Command): CommandJSON => {
const json: CommandJSON = {}
const name = command.name()

if (name) json.name = name
const version = command.version()
if (version) json.version = version
const description = command.description()
if (description) json.description = description
const usage = command.usage()
if (usage) json.usage = usage

json.arguments = command.registeredArguments.map(arg => {
const result = {name: arg.name()} as NonNullable<CommandJSON['arguments']>[number]

result.variadic = arg.variadic
result.required = arg.required

if (arg.description) result.description = arg.description
if (arg.defaultValue) result.defaultValue = arg.defaultValue as {}
if (arg.defaultValueDescription) result.defaultValueDescription = arg.defaultValueDescription
if (arg.argChoices) result.choices = arg.argChoices
return result
})

json.options = command.options.map(o => {
const result = {name: o.name()} as NonNullable<CommandJSON['options']>[number]

result.required = o.required
result.optional = o.optional
result.negate = o.negate
result.variadic = o.variadic

if (o.flags) result.flags = o.flags
if (o.short) result.short = o.short
if (o.description) result.description = o.description
if (o.argChoices) result.choices = o.argChoices

const attributeName = o.attributeName()
if (attributeName) result.attributeName = attributeName

if (o.defaultValue) result.defaultValue = o.defaultValue as {}
if (o.defaultValueDescription) result.defaultValueDescription = o.defaultValueDescription

return result
})

json.commands = command.commands.map(c => commandToJSON(c))

return json as {}
}
16 changes: 16 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type {JSONSchema7} from 'json-schema'
import {type JsonSchema7Type} from 'zod-to-json-schema'
import {CommandJSON} from './json'
import {AnyRouter, CreateCallerFactoryLike, inferRouterContext} from './trpc-compat'

// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand Down Expand Up @@ -177,15 +178,30 @@ export type TrpcCliRunParams = {
}
}

/**
* Type that looks like a `commander` Command instance, but doesn't require a dependency on `commander` to avoid awkward typescript errors.
* If you need to use it as a `Command` instance, just cast it with `as` to `import('commander').Command`.
*/
export type CommanderProgramLike = {
name: () => string
parseAsync: (args: string[], options?: {from: 'user' | 'node' | 'electron'}) => Promise<unknown>
helpInformation: () => string
}

export interface TrpcCli {
/** run the CLI - gets args from `process.argv` by default */
run: (params?: TrpcCliRunParams, program?: CommanderProgramLike) => Promise<void>
/**
* Build a `Commander` program from the CLI - you can use this to manually customise the program before passing it to `.run(...)`.
* Note that you will need to cast the return value to `import('commander').Command` to use it as a `Command` instance.
*/
buildProgram: (params?: TrpcCliRunParams) => CommanderProgramLike
/**
* @experimental
* Get a JSON representation of the CLI - useful for generating documentation etc. This function returns basic information about the CLI
* and each command - to get any extra details you will need to use the `buildProgram` function and walk the tree of commands yourself.
*/
toJSON: (program?: CommanderProgramLike) => CommandJSON
}

// todo: allow these all to be async?
Expand Down
Loading
0