Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions packages/gunshi/src/__snapshots__/cli.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,38 @@ OPTIONS:
"
`;

exports[`lazy command > command loading > config-command 1`] = `
"lazy-command-loading (lazy-command-loading v1.0.0)
Loaded configured command

USAGE:
lazy-command-loading config <OPTIONS>

OPTIONS:
-h, --help Display this help message
-v, --version Display this version
"
`;

exports[`lazy command > command loading > default-command 1`] = `
"lazy-command-loading (lazy-command-loading v1.0.0)
USAGE:
lazy-command-loading [COMMANDS] <OPTIONS>

COMMANDS:
<OPTIONS> CLI with dynamic commands
config <OPTIONS> Load command configuration

For more info, run any command with the \`--help\` flag:
lazy-command-loading --help
lazy-command-loading config --help

OPTIONS:
-h, --help Display this help message
-v, --version Display this version
"
`;

exports[`usageSilent 1`] = `
"Modern CLI tool (gunshi v0.0.0)
USAGE:
Expand Down
172 changes: 105 additions & 67 deletions packages/gunshi/src/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -317,80 +317,118 @@ describe('execute command', () => {
})
})

test('lazy command', async () => {
const mockEntry = vi.fn()
const entry = {
name: 'main',
run: mockEntry
}
const subCommands = new Map()

// lazy load function style command
const mockCommand1: Mocked<CommandRunner> = vi.fn()
const command1: LazyCommand = () => {
return new Promise<CommandRunner>(resolve => {
setTimeout(() => {
resolve(mockCommand1)
}, 5)
})
}
command1.commandName = 'command1'
command1.description = 'command1 description'
command1.args = {
foo: {
type: 'string',
short: 'f'
describe('lazy command', () => {
test('basic', async () => {
const mockEntry = vi.fn()
const entry = {
name: 'main',
run: mockEntry
}
}
subCommands.set(command1.commandName, command1)
const subCommands = new Map()

// lazy load object style command
const mockCommand2: Mocked<CommandRunner> = vi.fn()
const remoteCommand2: Command = {
name: 'command2',
description: 'command2 description',
args: {
bar: {
// lazy load function style command
const mockCommand1: Mocked<CommandRunner> = vi.fn()
const command1: LazyCommand = () => {
return new Promise<CommandRunner>(resolve => {
setTimeout(() => {
resolve(mockCommand1)
}, 5)
})
}
command1.commandName = 'command1'
command1.description = 'command1 description'
command1.args = {
foo: {
type: 'string',
short: 'b'
}
},
run: mockCommand2
}
const command2 = lazy(() => {
return new Promise<Command>(resolve => {
setTimeout(() => {
resolve(remoteCommand2)
}, 5)
})
}, remoteCommand2)
subCommands.set(command2.commandName, command2)

// regularly load command
const command3 = {
name: 'command3',
description: 'command3 description',
options: {
qux: {
type: 'number',
short: 'q'
short: 'f'
}
},
run: vi.fn()
}
subCommands.set(command3.name, command3)
}
subCommands.set(command1.commandName, command1)

const options = {
subCommands
}
// lazy load object style command
const mockCommand2: Mocked<CommandRunner> = vi.fn()
const remoteCommand2: Command = {
name: 'command2',
description: 'command2 description',
args: {
bar: {
type: 'string',
short: 'b'
}
},
run: mockCommand2
}
const command2 = lazy(() => {
return new Promise<Command>(resolve => {
setTimeout(() => {
resolve(remoteCommand2)
}, 5)
})
}, remoteCommand2)
subCommands.set(command2.commandName, command2)

// regularly load command
const command3 = {
name: 'command3',
description: 'command3 description',
options: {
qux: {
type: 'number',
short: 'q'
}
},
run: vi.fn()
}
subCommands.set(command3.name, command3)

const options = {
subCommands
}

await cli(['command1'], entry, options)
await cli(['command2'], entry, options)
await cli(['command3'], entry, options)
await cli(['command1'], entry, options)
await cli(['command2'], entry, options)
await cli(['command3'], entry, options)

expect(mockCommand1).toBeCalledTimes(1)
expect(mockCommand2).toBeCalledTimes(1)
expect(command3.run).toBeCalledTimes(1)
expect(mockCommand1).toBeCalledTimes(1)
expect(mockCommand2).toBeCalledTimes(1)
expect(command3.run).toBeCalledTimes(1)
})

test('command loading', async () => {
const utils = await import('./utils.ts')
defineMockLog(utils)

const configLoader = async () => {
return define({
description: 'Loaded configured command',
args: {
verbose: {
type: 'boolean',
description: 'Enable verbose output',
default: false
}
},
run(ctx) {
console.log('Run configured command:', ctx.values.verbose)
}
})
}
const lazyConfig = lazy(configLoader, {
name: 'config',
description: 'Load command configuration'
})
const subCommands = { [lazyConfig.commandName!]: lazyConfig }
const entry = define({
description: 'CLI with dynamic commands',
run: () => {}
})
const options = { name: 'lazy-command-loading', version: '1.0.0', subCommands }
const renderedDefaultUsage = await cli(['-h'], entry, options)
const renderedConfigUsage = await cli(['config', '-h'], entry, options)

expect(renderedDefaultUsage).toMatchSnapshot('default-command')
expect(renderedConfigUsage).toMatchSnapshot('config-command')
})
})

describe('auto generate usage', () => {
Expand Down
15 changes: 9 additions & 6 deletions packages/gunshi/src/cli/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,11 @@ export async function cliCore<G extends GunshiParamsConstraint = DefaultGunshiPa
skipPositional: callMode === 'subCommand' && cliOptions.subCommands!.size > 0 ? 0 : -1
})
const omitted = !subCommand

const resolvedCommand = isLazyCommand<G>(command)
? await resolveLazyCommand<G>(command, name, true)
: command

const commandContext = await createCommandContext({
args,
explicit,
Expand All @@ -90,13 +95,13 @@ export async function cliCore<G extends GunshiParamsConstraint = DefaultGunshiPa
tokens,
omitted,
callMode,
command,
command: resolvedCommand,
extensions: getPluginExtensions(resolvedPlugins),
validationError: error,
cliOptions: cliOptions
})

return await executeCommand(command, commandContext, name || '', decorators.commandDecorators)
return await executeCommand(resolvedCommand, commandContext, decorators.commandDecorators)
}

async function applyPlugins<G extends GunshiParamsConstraint>(
Expand Down Expand Up @@ -306,13 +311,11 @@ function getPluginExtensions(plugins: Plugin[]): Record<string, CommandContextEx
}

async function executeCommand<G extends GunshiParamsConstraint = DefaultGunshiParams>(
cmd: Command<G> | LazyCommand<G>,
cmd: Command<G>,
ctx: Readonly<CommandContext<G>>,
name: string,
decorators: Readonly<CommandDecorator<G>[]>
): Promise<string | undefined> {
const resolved = isLazyCommand<G>(cmd) ? await resolveLazyCommand<G>(cmd, name, true) : cmd
const baseRunner = resolved.run || NOOP
const baseRunner = cmd.run || NOOP

// apply plugin decorators
const decoratedRunner = decorators.reduceRight(
Expand Down
Loading