@gunshi/docs 0.27.0-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +20 -0
- package/package.json +52 -0
- package/src/guide/advanced/advanced-lazy-loading.md +312 -0
- package/src/guide/advanced/command-hooks.md +469 -0
- package/src/guide/advanced/context-extensions.md +545 -0
- package/src/guide/advanced/custom-rendering.md +945 -0
- package/src/guide/advanced/docs-gen.md +594 -0
- package/src/guide/advanced/internationalization.md +677 -0
- package/src/guide/advanced/type-system.md +561 -0
- package/src/guide/essentials/auto-usage.md +281 -0
- package/src/guide/essentials/composable.md +332 -0
- package/src/guide/essentials/declarative.md +724 -0
- package/src/guide/essentials/getting-started.md +252 -0
- package/src/guide/essentials/lazy-async.md +408 -0
- package/src/guide/essentials/plugin-system.md +472 -0
- package/src/guide/essentials/type-safe.md +154 -0
- package/src/guide/introduction/setup.md +68 -0
- package/src/guide/introduction/what-is-gunshi.md +68 -0
- package/src/guide/plugin/decorators.md +545 -0
- package/src/guide/plugin/dependencies.md +519 -0
- package/src/guide/plugin/extensions.md +317 -0
- package/src/guide/plugin/getting-started.md +298 -0
- package/src/guide/plugin/guidelines.md +940 -0
- package/src/guide/plugin/introduction.md +294 -0
- package/src/guide/plugin/lifecycle.md +432 -0
- package/src/guide/plugin/list.md +37 -0
- package/src/guide/plugin/testing.md +843 -0
- package/src/guide/plugin/type-system.md +529 -0
- package/src/index.md +44 -0
- package/src/release/v0.27.md +722 -0
- package/src/showcase.md +11 -0
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
# Getting Started
|
|
2
|
+
|
|
3
|
+
This guide will help you create your first command-line application with Gunshi.
|
|
4
|
+
|
|
5
|
+
We'll start with a simple "Hello World" example and gradually explore more features.
|
|
6
|
+
|
|
7
|
+
## Hello World Example
|
|
8
|
+
|
|
9
|
+
Let's create a simple CLI application that greets the user.
|
|
10
|
+
|
|
11
|
+
Create a new file (e.g., `cli.js` or `cli.ts`) and add the following code:
|
|
12
|
+
|
|
13
|
+
```js [cli.js]
|
|
14
|
+
import { cli } from 'gunshi'
|
|
15
|
+
|
|
16
|
+
// Run a simple command
|
|
17
|
+
await cli(process.argv.slice(2), () => {
|
|
18
|
+
console.log('Hello, World!')
|
|
19
|
+
})
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
<!-- eslint-disable markdown/no-missing-label-refs -->
|
|
23
|
+
|
|
24
|
+
> [!TIP]
|
|
25
|
+
> The example fully code is [here](https://github.com/kazupon/gunshi/tree/main/playground/essentials/getting-started/hello).
|
|
26
|
+
|
|
27
|
+
<!-- eslint-enable markdown/no-missing-label-refs -->
|
|
28
|
+
|
|
29
|
+
This minimal example demonstrates the core concept of Gunshi.
|
|
30
|
+
|
|
31
|
+
The `cli` function takes command-line arguments and a function to execute.
|
|
32
|
+
|
|
33
|
+
## Running Your CLI
|
|
34
|
+
|
|
35
|
+
You can run your CLI application with:
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
node cli.js
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
You should see the output:
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
Hello, World!
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Adding Command-Line Arguments
|
|
48
|
+
|
|
49
|
+
Let's enhance our example to accept a name as an argument.
|
|
50
|
+
|
|
51
|
+
The function receives a `CommandContext` object (abbreviated as `ctx`) as its parameter.
|
|
52
|
+
|
|
53
|
+
This context object contains parsed command-line arguments, options, and other execution information:
|
|
54
|
+
|
|
55
|
+
```js [cli.js]
|
|
56
|
+
import { cli } from 'gunshi'
|
|
57
|
+
|
|
58
|
+
await cli(process.argv.slice(2), ctx => {
|
|
59
|
+
// Access positional arguments
|
|
60
|
+
const name = ctx.positionals[0] || 'World'
|
|
61
|
+
console.log(`Hello, ${name}!`)
|
|
62
|
+
})
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
<!-- eslint-disable markdown/no-missing-label-refs -->
|
|
66
|
+
|
|
67
|
+
> [!TIP]
|
|
68
|
+
> The example fully code is [here](https://github.com/kazupon/gunshi/tree/main/playground/essentials/getting-started/context).
|
|
69
|
+
|
|
70
|
+
<!-- eslint-enable markdown/no-missing-label-refs -->
|
|
71
|
+
|
|
72
|
+
Now you can run:
|
|
73
|
+
|
|
74
|
+
```sh
|
|
75
|
+
node cli.js Alice
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
And you'll see:
|
|
79
|
+
|
|
80
|
+
```sh
|
|
81
|
+
Hello, Alice!
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Adding Command Options
|
|
85
|
+
|
|
86
|
+
Let's add some options to our command:
|
|
87
|
+
|
|
88
|
+
```js [cli.js]
|
|
89
|
+
import { cli } from 'gunshi'
|
|
90
|
+
|
|
91
|
+
const command = {
|
|
92
|
+
name: 'greeter',
|
|
93
|
+
description: 'A simple greeting CLI',
|
|
94
|
+
args: {
|
|
95
|
+
name: {
|
|
96
|
+
type: 'string',
|
|
97
|
+
short: 'n',
|
|
98
|
+
description: 'Name to greet'
|
|
99
|
+
},
|
|
100
|
+
uppercase: {
|
|
101
|
+
type: 'boolean',
|
|
102
|
+
short: 'u',
|
|
103
|
+
description: 'Convert greeting to uppercase'
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
run: ctx => {
|
|
107
|
+
const { name = 'World', uppercase } = ctx.values
|
|
108
|
+
let greeting = `Hello, ${name}!`
|
|
109
|
+
|
|
110
|
+
if (uppercase) {
|
|
111
|
+
greeting = greeting.toUpperCase()
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
console.log(greeting)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
await cli(process.argv.slice(2), command)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
<!-- eslint-disable markdown/no-missing-label-refs -->
|
|
122
|
+
|
|
123
|
+
> [!TIP]
|
|
124
|
+
> The example fully code is [here](https://github.com/kazupon/gunshi/tree/main/playground/essentials/getting-started/options).
|
|
125
|
+
|
|
126
|
+
<!-- eslint-enable markdown/no-missing-label-refs -->
|
|
127
|
+
|
|
128
|
+
Now you can run:
|
|
129
|
+
|
|
130
|
+
```sh
|
|
131
|
+
node cli.js --name Alice --uppercase
|
|
132
|
+
# or with short options
|
|
133
|
+
node cli.js -n Alice -u
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
And you'll see:
|
|
137
|
+
|
|
138
|
+
```sh
|
|
139
|
+
HELLO, ALICE!
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Built-in Help
|
|
143
|
+
|
|
144
|
+
Gunshi automatically generates help information for your commands through its built-in plugin system.
|
|
145
|
+
|
|
146
|
+
Run:
|
|
147
|
+
|
|
148
|
+
```sh
|
|
149
|
+
node cli.js --help
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
You'll see a help message that includes:
|
|
153
|
+
|
|
154
|
+
Here's an example of the generated help output:
|
|
155
|
+
|
|
156
|
+
```sh
|
|
157
|
+
USAGE:
|
|
158
|
+
COMMAND <OPTIONS>
|
|
159
|
+
|
|
160
|
+
OPTIONS:
|
|
161
|
+
-h, --help Display this help message
|
|
162
|
+
-v, --version Display this version
|
|
163
|
+
-n, --name <name> Name to greet
|
|
164
|
+
-u, --uppercase Convert greeting to uppercase
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
The help message automatically includes:
|
|
168
|
+
|
|
169
|
+
- Command description
|
|
170
|
+
- Available options
|
|
171
|
+
- Option descriptions
|
|
172
|
+
|
|
173
|
+
The standard `cli()` function automatically includes these built-in plugins:
|
|
174
|
+
|
|
175
|
+
- `@gunshi/plugin-global` - Provides global options like `--help` and `--version`
|
|
176
|
+
- `@gunshi/plugin-renderer` - Handles formatted output for help messages, error messages, and usage information
|
|
177
|
+
|
|
178
|
+
These plugins are included by default when you use `cli()` from the main `gunshi` package. If you use the lower-level `run()` function instead, you'll need to manually configure these plugins to get help and version functionality.
|
|
179
|
+
|
|
180
|
+
<!-- eslint-disable markdown/no-missing-label-refs -->
|
|
181
|
+
|
|
182
|
+
> [!TIP]
|
|
183
|
+
> Want to learn more about Gunshi's plugin architecture? Check out the [Plugin System guide](./plugin-system.md) to understand how plugins work, explore the built-in plugins in detail, and learn how to create your own custom plugins to extend your CLI's functionality.
|
|
184
|
+
|
|
185
|
+
<!-- eslint-enable markdown/no-missing-label-refs -->
|
|
186
|
+
|
|
187
|
+
## Using Gunshi with Different Runtimes
|
|
188
|
+
|
|
189
|
+
Gunshi is designed to work seamlessly across multiple JavaScript runtimes. Here's how to use it with each supported environment:
|
|
190
|
+
|
|
191
|
+
### Node.js
|
|
192
|
+
|
|
193
|
+
For Node.js applications, use `process.argv.slice(2)` to pass command-line arguments:
|
|
194
|
+
|
|
195
|
+
```js [cli.js]
|
|
196
|
+
import { cli } from 'gunshi'
|
|
197
|
+
|
|
198
|
+
function entry() {
|
|
199
|
+
console.log('Hello, Gunshi!')
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
await cli(process.argv.slice(2), entry)
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Deno
|
|
206
|
+
|
|
207
|
+
In Deno, use `Deno.args` to access command-line arguments:
|
|
208
|
+
|
|
209
|
+
```ts [cli.ts]
|
|
210
|
+
import { cli } from '@gunshi/gunshi'
|
|
211
|
+
|
|
212
|
+
function entry() {
|
|
213
|
+
console.log('Hello, Gunshi with Deno!')
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
await cli(Deno.args, entry)
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### Bun
|
|
220
|
+
|
|
221
|
+
Bun also provides `Bun.argv` similar to Node.js:
|
|
222
|
+
|
|
223
|
+
```ts [cli.ts]
|
|
224
|
+
import { cli } from 'gunshi'
|
|
225
|
+
|
|
226
|
+
function entry() {
|
|
227
|
+
console.log('Hello, Gunshi with Bun!')
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
await cli(Bun.argv.slice(2), entry) // or use process.argv.slice(2) in Bun
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Note that while the argument passing differs slightly between runtimes, the Gunshi API remains consistent across all environments.
|
|
234
|
+
|
|
235
|
+
## Next Steps
|
|
236
|
+
|
|
237
|
+
You've successfully created your first Gunshi CLI application! You've learned the fundamentals: creating basic commands, handling arguments and options, using the built-in help system, and running your CLI across different JavaScript runtimes.
|
|
238
|
+
|
|
239
|
+
Now it's time to explore the essential features that will help you build powerful, production-ready CLI applications.
|
|
240
|
+
|
|
241
|
+
The following chapters will guide you through each topic:
|
|
242
|
+
|
|
243
|
+
- **[Declarative Configuration](./declarative.md)** - Organize commands with clear, maintainable declarative structures
|
|
244
|
+
- **[Type Safety](./type-safe.md)** - Leverage TypeScript for automatic type inference and compile-time checking
|
|
245
|
+
- **[Composable Sub-commands](./composable.md)** - Build complex CLIs with modular sub-commands like `git commit` or `npm install`
|
|
246
|
+
- **[Lazy & Async Command Loading](./lazy-async.md)** - Optimize startup performance by loading commands only when needed
|
|
247
|
+
- **[Auto Usage Generation](./auto-usage.md)** - Create self-documenting CLIs with automatic help and usage information
|
|
248
|
+
- **[Plugin System](./plugin-system.md)** - Extend your CLI with modular plugins for features like i18n and shell completion
|
|
249
|
+
|
|
250
|
+
Each chapter builds upon the previous ones, introducing more sophisticated patterns and techniques.
|
|
251
|
+
|
|
252
|
+
Start with [Declarative Configuration](./declarative.md) to learn how to structure your commands in a clean, maintainable way as your CLI grows in complexity.
|
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
# Lazy & Async Command Loading
|
|
2
|
+
|
|
3
|
+
<!-- eslint-disable markdown/no-missing-label-refs -->
|
|
4
|
+
|
|
5
|
+
> [!NOTE]
|
|
6
|
+
> This chapter continues using TypeScript for code examples, building upon the type safety concepts introduced in the [previous chapter](./type-safe.md). While all Gunshi features work with JavaScript, the TypeScript examples provide better IDE support and compile-time checking.
|
|
7
|
+
|
|
8
|
+
<!-- eslint-enable markdown/no-missing-label-refs -->
|
|
9
|
+
|
|
10
|
+
When building CLI applications with many commands or commands that require heavy dependencies, you may encounter slow startup times that frustrate users.
|
|
11
|
+
|
|
12
|
+
Even simple operations like displaying help text can take seconds if your CLI loads all command implementations upfront.
|
|
13
|
+
|
|
14
|
+
This guide shows you how to use Gunshi's lazy loading and asynchronous execution features to create fast, responsive CLI applications that only load what they need, when they need it.
|
|
15
|
+
|
|
16
|
+
## Benefits of Lazy Loading
|
|
17
|
+
|
|
18
|
+
Lazy loading provides immediate improvements to your CLI application:
|
|
19
|
+
|
|
20
|
+
- **Faster Startup Time**: Commands are only loaded when actually executed, not when displaying help or running other commands
|
|
21
|
+
- **Reduced Memory Usage**: Unexecuted commands and their dependencies stay unloaded, keeping memory footprint minimal
|
|
22
|
+
- **Better Code Splitting**: When bundling your CLI, each command can be a separate chunk that loads on demand
|
|
23
|
+
- **Improved User Experience**: Users see instant help text and quick responses for simple commands
|
|
24
|
+
|
|
25
|
+
These benefits become especially valuable as your CLI grows to include dozens or hundreds of commands, each potentially requiring different libraries or complex initialization.
|
|
26
|
+
|
|
27
|
+
## Basic Lazy Loading
|
|
28
|
+
|
|
29
|
+
Let's start with a simple example that demonstrates the core concept.
|
|
30
|
+
|
|
31
|
+
The following code shows how to create a lazy-loaded command that only loads its implementation when executed:
|
|
32
|
+
|
|
33
|
+
```ts [cli.ts]
|
|
34
|
+
import { cli, define, lazy } from 'gunshi'
|
|
35
|
+
import type { CommandRunner, CommandContext } from 'gunshi'
|
|
36
|
+
|
|
37
|
+
// Define the command without command runner separately
|
|
38
|
+
const helloDefinition = define({
|
|
39
|
+
name: 'hello',
|
|
40
|
+
description: 'A greeting command',
|
|
41
|
+
args: {
|
|
42
|
+
name: {
|
|
43
|
+
type: 'string',
|
|
44
|
+
description: 'Name to greet',
|
|
45
|
+
default: 'world'
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
// Create a loader function that returns the command implementation
|
|
51
|
+
const helloLoader = async (): Promise<CommandRunner> => {
|
|
52
|
+
console.log('Loading hello command runner ...')
|
|
53
|
+
|
|
54
|
+
// The actual command runner logic is defined here
|
|
55
|
+
const run = (ctx: CommandContext) => {
|
|
56
|
+
console.log(`Hello, ${ctx.values.name}!`)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return run // Return the runner function
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Combine them using the lazy helper
|
|
63
|
+
const lazyHello = lazy(helloLoader, helloDefinition)
|
|
64
|
+
|
|
65
|
+
// Use the lazy command in your CLI
|
|
66
|
+
const subCommands = {
|
|
67
|
+
[lazyHello.commandName]: lazyHello
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
await cli(
|
|
71
|
+
process.argv.slice(2),
|
|
72
|
+
{
|
|
73
|
+
description: 'My CLI application',
|
|
74
|
+
run: () => console.log('Run a sub-command with --help for more info')
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: 'my-app',
|
|
78
|
+
version: '1.0.0',
|
|
79
|
+
subCommands
|
|
80
|
+
}
|
|
81
|
+
)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
<!-- eslint-disable markdown/no-missing-label-refs -->
|
|
85
|
+
|
|
86
|
+
> [!TIP]
|
|
87
|
+
> The example fully code is [here](https://github.com/kazupon/gunshi/tree/main/playground/essentials/lazy-async/basic).
|
|
88
|
+
|
|
89
|
+
<!-- eslint-enable markdown/no-missing-label-refs -->
|
|
90
|
+
|
|
91
|
+
In this example, when a user runs `npx tsx cli.ts --help`, Gunshi displays help text using only the metadata from `helloDefinition`.
|
|
92
|
+
|
|
93
|
+
The `helloLoader` function is never called.
|
|
94
|
+
|
|
95
|
+
Only when the user runs `npx tsx cli.ts hello` does Gunshi execute the loader and run the command.
|
|
96
|
+
|
|
97
|
+
<!-- eslint-disable markdown/no-missing-label-refs -->
|
|
98
|
+
|
|
99
|
+
> [!TIP]
|
|
100
|
+
> The command name is accessed via `commandName` property (not `name`) when using lazy commands. This is because lazy commands are functions, and the `name` property is reserved by the JavaScript runtime.
|
|
101
|
+
|
|
102
|
+
<!-- eslint-enable markdown/no-missing-label-refs -->
|
|
103
|
+
|
|
104
|
+
## Dynamic Imports for Code Splitting
|
|
105
|
+
|
|
106
|
+
For real-world applications, you'll typically want to use dynamic imports to load command implementations from separate files.
|
|
107
|
+
|
|
108
|
+
This enables bundlers to create separate chunks for each command:
|
|
109
|
+
|
|
110
|
+
```ts [cli.ts]
|
|
111
|
+
import { cli, define, lazy } from 'gunshi'
|
|
112
|
+
import type { CommandRunner } from 'gunshi'
|
|
113
|
+
|
|
114
|
+
// Command metadata stays in the main bundle
|
|
115
|
+
const buildDefinition = define({
|
|
116
|
+
name: 'build',
|
|
117
|
+
description: 'Build the project',
|
|
118
|
+
args: {
|
|
119
|
+
watch: {
|
|
120
|
+
type: 'boolean',
|
|
121
|
+
short: 'w',
|
|
122
|
+
description: 'Watch for changes'
|
|
123
|
+
},
|
|
124
|
+
minify: {
|
|
125
|
+
type: 'boolean',
|
|
126
|
+
short: 'm',
|
|
127
|
+
description: 'Minify output'
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
// Loader uses dynamic import to load the command from a separate file
|
|
133
|
+
const buildLoader = async (): Promise<CommandRunner> => {
|
|
134
|
+
// This creates a separate chunk in your bundle
|
|
135
|
+
const { run } = await import('./commands/build.ts')
|
|
136
|
+
return run
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const lazyBuild = lazy(buildLoader, buildDefinition)
|
|
140
|
+
|
|
141
|
+
// Add more commands following the same pattern
|
|
142
|
+
const deployDefinition = define({
|
|
143
|
+
name: 'deploy',
|
|
144
|
+
description: 'Deploy to production',
|
|
145
|
+
args: {
|
|
146
|
+
environment: {
|
|
147
|
+
type: 'string',
|
|
148
|
+
short: 'e',
|
|
149
|
+
default: 'production'
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
const deployLoader = async (): Promise<CommandRunner> => {
|
|
155
|
+
const { run } = await import('./commands/deploy.ts')
|
|
156
|
+
return run
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const lazyDeploy = lazy(deployLoader, deployDefinition)
|
|
160
|
+
|
|
161
|
+
// Register all lazy commands
|
|
162
|
+
const subCommands = {
|
|
163
|
+
[lazyBuild.commandName]: lazyBuild,
|
|
164
|
+
[lazyDeploy.commandName]: lazyDeploy
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
await cli(
|
|
168
|
+
process.argv.slice(2),
|
|
169
|
+
{
|
|
170
|
+
description: 'Development tools CLI',
|
|
171
|
+
run: () => console.log('Use --help to see available commands')
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
name: 'dev-tools',
|
|
175
|
+
version: '2.0.0',
|
|
176
|
+
subCommands
|
|
177
|
+
}
|
|
178
|
+
)
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
<!-- eslint-disable markdown/no-missing-label-refs -->
|
|
182
|
+
|
|
183
|
+
> [!TIP]
|
|
184
|
+
> The example fully code is [here](https://github.com/kazupon/gunshi/tree/main/playground/essentials/lazy-async/dynamic-import).
|
|
185
|
+
|
|
186
|
+
<!-- eslint-enable markdown/no-missing-label-refs -->
|
|
187
|
+
|
|
188
|
+
<!-- eslint-disable markdown/no-missing-label-refs, markdown-preferences/padding-line-between-blocks -->
|
|
189
|
+
|
|
190
|
+
> [!IMPORTANT]
|
|
191
|
+
> The examples above use `.ts` extensions in dynamic import paths (`await import('./commands/build.ts')`). This will work in the following scenarios:
|
|
192
|
+
>
|
|
193
|
+
> - **During development**: When using TypeScript execution tools like `tsx`, `ts-node`, or Bun's native TypeScript support
|
|
194
|
+
> - **Node.js with experimental support**: When running Node.js v22.6.0+ with the `--experimental-strip-types` flag (renamed to `--experimental-transform-types` in v22.7.0+)
|
|
195
|
+
>
|
|
196
|
+
> **For production builds with TypeScript compilation**:
|
|
197
|
+
> TypeScript does NOT rewrite import paths during compilation. If you plan to compile your TypeScript:
|
|
198
|
+
>
|
|
199
|
+
> - Use `.js` extensions in your source code: `await import('./commands/build.js')`
|
|
200
|
+
> - These `.js` imports will correctly resolve to the compiled JavaScript files
|
|
201
|
+
> - Alternatively, configure your bundler (rolldown, rollup, esbuild, webpack) to handle `.ts` extensions
|
|
202
|
+
>
|
|
203
|
+
> For standard JavaScript projects, use `.js` extensions:
|
|
204
|
+
>
|
|
205
|
+
> ```ts
|
|
206
|
+
> const { run } = await import('./commands/build.js')
|
|
207
|
+
> ```
|
|
208
|
+
|
|
209
|
+
<!-- eslint-enable markdown/no-missing-label-refs, markdown-preferences/padding-line-between-blocks -->
|
|
210
|
+
|
|
211
|
+
With this approach, your bundler (rolldown, rollup, esbuild, webpack, etc.) will automatically create separate chunks for `./commands/build.ts` and `./commands/deploy.ts`. Users only download and parse the code for commands they actually use.
|
|
212
|
+
|
|
213
|
+
<!-- eslint-disable markdown/no-missing-label-refs -->
|
|
214
|
+
|
|
215
|
+
> [!NOTE]
|
|
216
|
+
> On Node.js v23.6.0 and newer, type stripping is enabled by default so erasable TypeScript runs without flags; transformations (e.g., `enum`) still require `--experimental-transform-types`.
|
|
217
|
+
|
|
218
|
+
<!-- eslint-enable markdown/no-missing-label-refs -->
|
|
219
|
+
|
|
220
|
+
## Async Command Execution
|
|
221
|
+
|
|
222
|
+
Gunshi seamlessly supports asynchronous command execution, which is essential for commands that perform I/O operations, network requests, or other async tasks. Building on lazy loading, your command runners can be async functions:
|
|
223
|
+
|
|
224
|
+
```ts [cli.ts]
|
|
225
|
+
import { cli, define, lazy } from 'gunshi'
|
|
226
|
+
import type { Command, CommandContext, CommandRunner, GunshiParams } from 'gunshi'
|
|
227
|
+
|
|
228
|
+
// Mock implementations that simulate async file operations
|
|
229
|
+
// These work without requiring actual files on disk
|
|
230
|
+
async function readFile(path: string): Promise<string> {
|
|
231
|
+
// Simulate async file read with a small delay
|
|
232
|
+
await new Promise(resolve => setTimeout(resolve, 100))
|
|
233
|
+
return `Sample data from ${path}`
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async function transform(data: string): Promise<string> {
|
|
237
|
+
// Simulate async data transformation
|
|
238
|
+
await new Promise(resolve => setTimeout(resolve, 150))
|
|
239
|
+
return data.toUpperCase() + '\n[TRANSFORMED]'
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async function writeFile(path: string, data: string): Promise<void> {
|
|
243
|
+
// Simulate async file write
|
|
244
|
+
await new Promise(resolve => setTimeout(resolve, 100))
|
|
245
|
+
console.log(` Written to ${path}: "${data}"`)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Alternative: Use real Node.js file operations
|
|
249
|
+
// import { readFile as fsReadFile, writeFile as fsWriteFile } from 'node:fs/promises'
|
|
250
|
+
//
|
|
251
|
+
// async function readFile(path: string): Promise<string> {
|
|
252
|
+
// return await fsReadFile(path, 'utf-8')
|
|
253
|
+
// }
|
|
254
|
+
//
|
|
255
|
+
// async function transform(data: string): Promise<string> {
|
|
256
|
+
// // Your actual transformation logic here
|
|
257
|
+
// return data.toUpperCase()
|
|
258
|
+
// }
|
|
259
|
+
//
|
|
260
|
+
// async function writeFile(path: string, data: string): Promise<void> {
|
|
261
|
+
// await fsWriteFile(path, data, 'utf-8')
|
|
262
|
+
// }
|
|
263
|
+
|
|
264
|
+
const processDataDefinition = define({
|
|
265
|
+
name: 'process',
|
|
266
|
+
description: 'Process data asynchronously',
|
|
267
|
+
args: {
|
|
268
|
+
input: {
|
|
269
|
+
type: 'string',
|
|
270
|
+
short: 'i',
|
|
271
|
+
description: 'Input file path',
|
|
272
|
+
required: true
|
|
273
|
+
},
|
|
274
|
+
output: {
|
|
275
|
+
type: 'string',
|
|
276
|
+
short: 'o',
|
|
277
|
+
description: 'Output file path',
|
|
278
|
+
required: true
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
const processDataLoader = async (): Promise<
|
|
284
|
+
CommandRunner<GunshiParams<{ args: typeof processDataDefinition.args }>>
|
|
285
|
+
> => {
|
|
286
|
+
// Return an async runner function
|
|
287
|
+
return async ctx => {
|
|
288
|
+
// TypeScript knows ctx.values has 'input' (string) and 'output' (string)
|
|
289
|
+
const { input, output } = ctx.values
|
|
290
|
+
|
|
291
|
+
console.log(`Processing ${input}...`)
|
|
292
|
+
|
|
293
|
+
// Simulate async operations
|
|
294
|
+
const data = await readFile(input)
|
|
295
|
+
const processed = await transform(data)
|
|
296
|
+
await writeFile(output, processed)
|
|
297
|
+
|
|
298
|
+
console.log(`Successfully processed to ${output}`)
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const lazyProcess = lazy(processDataLoader, processDataDefinition)
|
|
303
|
+
|
|
304
|
+
// The CLI handles async execution automatically
|
|
305
|
+
const subCommands = {
|
|
306
|
+
[lazyProcess.commandName]: lazyProcess
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
await cli(
|
|
310
|
+
process.argv.slice(2),
|
|
311
|
+
{
|
|
312
|
+
description: 'Data processing CLI',
|
|
313
|
+
run: () => console.log('Use process command to transform data')
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
name: 'data-cli',
|
|
317
|
+
version: '1.0.0',
|
|
318
|
+
subCommands
|
|
319
|
+
}
|
|
320
|
+
)
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
<!-- eslint-disable markdown/no-missing-label-refs -->
|
|
324
|
+
|
|
325
|
+
> [!TIP]
|
|
326
|
+
> The example fully code is [here](https://github.com/kazupon/gunshi/tree/main/playground/essentials/lazy-async/async).
|
|
327
|
+
|
|
328
|
+
<!-- eslint-enable markdown/no-missing-label-refs -->
|
|
329
|
+
|
|
330
|
+
Gunshi automatically handles the asynchronous execution, including proper error handling and process exit codes.
|
|
331
|
+
|
|
332
|
+
You don't need any special configuration—just return an async function from your loader.
|
|
333
|
+
|
|
334
|
+
## Alternative Loader Return Types
|
|
335
|
+
|
|
336
|
+
While the examples above show loaders returning runner functions, loaders can also return full Command objects.
|
|
337
|
+
|
|
338
|
+
This is useful when you want to dynamically construct the entire command structure:
|
|
339
|
+
|
|
340
|
+
```ts [cli.ts]
|
|
341
|
+
import { cli, define, lazy } from 'gunshi'
|
|
342
|
+
|
|
343
|
+
const configLoader = async () => {
|
|
344
|
+
// Simulate loading configuration (in practice, read from file/API)
|
|
345
|
+
const isDebug = process.env.DEBUG === 'true'
|
|
346
|
+
|
|
347
|
+
return define({
|
|
348
|
+
description: `Config command (debug: ${isDebug})`,
|
|
349
|
+
args: {
|
|
350
|
+
verbose: {
|
|
351
|
+
type: 'boolean',
|
|
352
|
+
description: isDebug ? 'Verbose output (DEBUG mode)' : 'Verbose output'
|
|
353
|
+
}
|
|
354
|
+
},
|
|
355
|
+
run: ctx => {
|
|
356
|
+
console.log(isDebug ? 'Running in DEBUG mode' : 'Running in normal mode')
|
|
357
|
+
if (ctx.values.verbose) {
|
|
358
|
+
console.log('Verbose:', ctx.values)
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
})
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Override command meta info with 2nd parameters
|
|
365
|
+
const config = lazy(configLoader, {
|
|
366
|
+
name: 'config',
|
|
367
|
+
description: 'Dynamically configured command'
|
|
368
|
+
})
|
|
369
|
+
|
|
370
|
+
await cli(
|
|
371
|
+
process.argv.slice(2),
|
|
372
|
+
{ description: 'CLI with dynamic commands', run: () => {} },
|
|
373
|
+
{ name: 'my-cli', version: '1.0.0', subCommands: { config } }
|
|
374
|
+
)
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
<!-- eslint-disable markdown/no-missing-label-refs -->
|
|
378
|
+
|
|
379
|
+
> [!TIP]
|
|
380
|
+
> The example fully code is [here](https://github.com/kazupon/gunshi/tree/main/playground/essentials/lazy-async/dynamic-command)
|
|
381
|
+
|
|
382
|
+
<!-- eslint-enable markdown/no-missing-label-refs -->
|
|
383
|
+
|
|
384
|
+
This pattern is particularly useful when command structure depends on runtime configuration or external data sources.
|
|
385
|
+
|
|
386
|
+
## Performance Considerations
|
|
387
|
+
|
|
388
|
+
To maximize the benefits of lazy loading:
|
|
389
|
+
|
|
390
|
+
1. **Keep loaders lightweight**: The loader function itself should be minimal. Put heavy imports and initialization inside the returned runner function or use dynamic imports.
|
|
391
|
+
|
|
392
|
+
2. **Group related commands**: If multiple commands share dependencies, consider loading them together to avoid redundant imports.
|
|
393
|
+
|
|
394
|
+
3. **Monitor bundle sizes**: Use your bundler's analysis tools to verify that commands are properly code-split into separate chunks.
|
|
395
|
+
|
|
396
|
+
4. **Consider startup frequency**: Commands that are frequently used together might benefit from being in the same chunk, while rarely-used commands should definitely be lazy-loaded.
|
|
397
|
+
|
|
398
|
+
## Next Steps
|
|
399
|
+
|
|
400
|
+
Now that you understand lazy loading and async execution in Gunshi, you've learned how to optimize your CLI's performance by loading commands only when needed.
|
|
401
|
+
|
|
402
|
+
You can create fast, responsive CLIs that handle complex asynchronous operations efficiently while keeping startup times minimal.
|
|
403
|
+
|
|
404
|
+
The next section on [Auto Usage Generation](./auto-usage.md) will show you how Gunshi automatically creates comprehensive help documentation for all your commands—including lazy-loaded ones.
|
|
405
|
+
|
|
406
|
+
You'll learn how to enhance your CLI's user experience with well-structured usage information, examples, and command descriptions.
|
|
407
|
+
|
|
408
|
+
With lazy loading keeping your CLI fast and auto-generated usage making it user-friendly, you'll have all the essential tools to build professional command-line applications with Gunshi.
|