Skip to content

Lazy & Async Command Loading

Gunshi supports lazy loading of commands and asynchronous execution, which can significantly improve the performance and responsiveness of your CLI applications, especially when dealing with many commands or resource-intensive operations.

Why Use Lazy Loading?

Lazy loading is beneficial when:

  • Your CLI has many commands, but users typically only use a few at a time
  • Some commands require heavy dependencies that aren't needed for other commands
  • You want to reduce the initial startup time of your CLI

Basic Lazy Loading

Here's how to implement lazy loading in Gunshi:

js
import { cli } from 'gunshi'

// Define a command that will be loaded lazily
const lazyCommand = async () => {
  console.log('Loading lazy command...')

  // You could import heavy dependencies here
  // const heavyDependency = await import('heavy-dependency')

  // Return the actual command
  return {
    name: 'lazy',
    description: 'A command that is loaded lazily',
    run: ctx => {
      console.log('Lazy command executed!')
    }
  }
}

// Create a Map of sub-commands with lazy-loaded commands
const subCommands = new Map()
subCommands.set('lazy', lazyCommand)

// Define the main command
const mainCommand = {
  name: 'lazy-example',
  description: 'Example of lazy loading',
  run: () => {
    console.log('Use the lazy sub-command')
  }
}

// Run the CLI with lazy-loaded commands
await cli(process.argv.slice(2), mainCommand, {
  name: 'my-app',
  version: '1.0.0',
  subCommands
})

In this example, the lazyCommand is a function that returns a Promise that resolves to the actual command. The command is only loaded when the user runs the corresponding sub-command.

Async Command Execution

Gunshi also supports asynchronous command execution, which is useful for commands that need to perform asynchronous operations:

js
import { cli } from 'gunshi'

const command = {
  name: 'async-example',
  description: 'Example of async command execution',
  options: {
    delay: {
      type: 'number',
      default: 1000,
      description: 'Delay in milliseconds'
    }
  },
  run: async ctx => {
    const { delay } = ctx.values

    console.log(`Waiting for ${delay}ms...`)

    // Perform an async operation
    await new Promise(resolve => setTimeout(resolve, delay))

    console.log('Async operation completed!')
  }
}

await cli(process.argv.slice(2), command)

Combining Lazy Loading and Async Execution

You can combine lazy loading and async execution for maximum flexibility:

js
import { cli } from 'gunshi'

// Define a command that will be loaded lazily
const lazyCommand = async () => {
  console.log('Loading lazy command...')

  // Simulate a delay to demonstrate async loading
  await new Promise(resolve => setTimeout(resolve, 1000))

  console.log('Lazy command loaded!')

  // Return the actual command with async execution
  return {
    name: 'lazy',
    description: 'A command that is loaded lazily',
    options: {
      delay: {
        type: 'number',
        short: 'd',
        default: 500,
        description: 'Delay in milliseconds for command execution'
      }
    },
    run: async ctx => {
      const { delay } = ctx.values

      console.log(`Executing lazy command with ${delay}ms delay...`)

      // Simulate async work
      await new Promise(resolve => setTimeout(resolve, delay))

      console.log('Lazy command execution completed!')
    }
  }
}

// Create a Map of sub-commands with lazy-loaded commands
const subCommands = new Map()
subCommands.set('lazy', lazyCommand)

// Run the CLI with lazy-loaded commands
await cli(
  process.argv.slice(2),
  { name: 'main', run: () => {} },
  {
    name: 'lazy-async-example',
    version: '1.0.0',
    subCommands
  }
)

Loading Data Asynchronously

A common use case for lazy loading and async execution is fetching data from external sources:

js
import { cli } from 'gunshi'

// Define a command that loads data asynchronously
const dataCommand = async () => {
  console.log('Loading data command...')

  // Simulate fetching data from an API
  const fetchData = async () => {
    console.log('Fetching data...')
    await new Promise(resolve => setTimeout(resolve, 800))
    return [
      { id: 1, name: 'Item 1' },
      { id: 2, name: 'Item 2' },
      { id: 3, name: 'Item 3' }
    ]
  }

  // Fetch the data asynchronously
  const data = await fetchData()

  console.log('Data command loaded with data!')

  // Return the command with the fetched data
  return {
    name: 'data',
    description: 'A command that loads data asynchronously',
    options: {
      id: {
        type: 'number',
        short: 'i',
        description: 'Filter by item ID'
      }
    },
    run: ctx => {
      const { id } = ctx.values

      console.log('Available data:')

      if (id) {
        const item = data.find(item => item.id === id)
        if (item) {
          console.log(`- ${item.id}: ${item.name}`)
        } else {
          console.log(`No item found with ID: ${id}`)
        }
      } else {
        for (const item of data) {
          console.log(`- ${item.id}: ${item.name}`)
        }
      }
    }
  }
}

// Create a Map of sub-commands
const subCommands = new Map()
subCommands.set('data', dataCommand)

// Run the CLI
await cli(
  process.argv.slice(2),
  { name: 'main', run: () => {} },
  {
    name: 'data-example',
    version: '1.0.0',
    subCommands
  }
)

In this example, the data is fetched asynchronously when the command is loaded, and then made available to the command's run function.

Type-Safe Async Commands

For async commands, you can use TypeScript's async/await with proper typing:

ts
import { cli } from 'gunshi'
import type { ArgOptions, Command, CommandContext } from 'gunshi'

// Define a type-safe async command
const command: Command<ArgOptions> = {
  name: 'async-example',
  options: {
    delay: {
      type: 'number',
      default: 1000
    }
  },
  run: async (ctx: CommandContext<ArgOptions>) => {
    const { delay } = ctx.values

    console.log(`Waiting for ${delay}ms...`)
    await new Promise(resolve => setTimeout(resolve, delay as number))
    console.log('Done!')
  }
}

// Execute the async command
await cli(process.argv.slice(2), command)

Performance Benefits

Lazy loading and async execution can provide significant performance benefits:

  1. Faster startup time: Only load the commands that are actually used
  2. Reduced memory usage: Avoid loading unnecessary dependencies
  3. Better responsiveness: Perform heavy operations asynchronously without blocking the main thread
  4. Improved user experience: Provide feedback during long-running operations

Released under the MIT License.