@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,68 @@
|
|
|
1
|
+
# What's Gunshi?
|
|
2
|
+
|
|
3
|
+
Gunshi is a modern JavaScript command-line library designed to simplify the creation of command-line interfaces (CLIs).
|
|
4
|
+
|
|
5
|
+
## Origin of the Name
|
|
6
|
+
|
|
7
|
+
The name "gunshi" (č»åø«) refers to a position in ancient Japanese samurai battles where samurai devised strategies and gave orders. This name is inspired by the word "command", reflecting the library's purpose of handling command-line commands.
|
|
8
|
+
|
|
9
|
+
## Key Features
|
|
10
|
+
|
|
11
|
+
Gunshi is designed with several powerful features to make CLI development easier and more maintainable:
|
|
12
|
+
|
|
13
|
+
- š **Simple & Universal**: Run commands with simple API and support for universal runtime (Node.js, Deno, Bun).
|
|
14
|
+
- āļø **Declarative & Type Safe**: Configure commands declaratively with full TypeScript support and type-safe argument parsing by [args-tokens](https://github.com/kazupon/args-tokens)
|
|
15
|
+
- š§© **Composable & Lazy**: Create modular sub-commands with context sharing and lazy loading for better performance.
|
|
16
|
+
- šØ **Flexible Rendering**: Customize usage generation, validation errors, and help messages with pluggable renderers.
|
|
17
|
+
- š **Internationalization**: Built with global users in mind, featuring locale-aware design, resource management, and multi-language support.
|
|
18
|
+
- š **Pluggable**: Extensible plugin system with dependency management and lifecycle hooks for modular CLI development.
|
|
19
|
+
|
|
20
|
+
## Why Gunshi?
|
|
21
|
+
|
|
22
|
+
Gunshi provides a modern approach to building command-line interfaces in JavaScript and TypeScript. It's designed to be:
|
|
23
|
+
|
|
24
|
+
- **Developer-friendly**: Simple API with TypeScript support
|
|
25
|
+
- **Flexible**: Compose commands and customize behavior as needed
|
|
26
|
+
- **Maintainable**: Declarative configuration makes code easier to understand and maintain
|
|
27
|
+
- **Performant**: Lazy loading ensures resources are only loaded when needed
|
|
28
|
+
|
|
29
|
+
Whether you're building a simple CLI tool or a complex command-line application with multiple sub-commands, Gunshi provides the features you need to create a great user experience.
|
|
30
|
+
|
|
31
|
+
## Next Steps
|
|
32
|
+
|
|
33
|
+
Now that you understand what Gunshi is and its key features, here's how to proceed with the documentation:
|
|
34
|
+
|
|
35
|
+
### Documentation Structure
|
|
36
|
+
|
|
37
|
+
The Gunshi documentation is organized into three main sections:
|
|
38
|
+
|
|
39
|
+
- **Essentials**: Learn the fundamental concepts of Gunshi through a step-by-step tutorial format. This section covers everything from basic usage to composable commands and lazy loading.
|
|
40
|
+
|
|
41
|
+
- **Advanced**: Explore specialized features organized by topic. Each chapter focuses on a specific advanced capability like internationalization, custom rendering, or type system extensions.
|
|
42
|
+
|
|
43
|
+
- **Plugin**: Understand the plugin system through a tutorial approach. Learn how to create, configure, and distribute plugins for extending Gunshi's functionality.
|
|
44
|
+
|
|
45
|
+
### Where to Go Next
|
|
46
|
+
|
|
47
|
+
<!-- eslint-disable markdown/no-missing-label-refs -->
|
|
48
|
+
|
|
49
|
+
> [!TIP]
|
|
50
|
+
> Start with the **Setup** guide to install Gunshi in your project, then proceed through the **Essentials** section in order. Each chapter builds upon previous concepts.
|
|
51
|
+
|
|
52
|
+
<!-- eslint-enable markdown/no-missing-label-refs -->
|
|
53
|
+
|
|
54
|
+
1. **[Setup](./setup.md)** - Install and configure Gunshi in your project
|
|
55
|
+
2. **[Getting Started](../essentials/getting-started.md)** - Create your first CLI application
|
|
56
|
+
3. Continue through the **Essentials** section to learn core concepts
|
|
57
|
+
|
|
58
|
+
After completing the essentials, you can explore the **Advanced** section based on your specific needs. The chapters there are independent and can be read in any order.
|
|
59
|
+
|
|
60
|
+
If you're interested in extending Gunshi with plugins, the **Plugin** section provides comprehensive guidance on creating and using plugins.
|
|
61
|
+
|
|
62
|
+
## Credits
|
|
63
|
+
|
|
64
|
+
Gunshi project is made possible by the open-source community and the many innovative tools in the JavaScript and TypeScript ecosystem. We extend our gratitude to all contributors and maintainers whose work has laid the foundation for this project.
|
|
65
|
+
|
|
66
|
+
- [`citty`](https://github.com/unjs/citty), Provided the original inspiration for modern JavaScript command-line library.
|
|
67
|
+
- [`ordana`](https://github.com/sapphi-red/ordana), Inspired documentation generation in Gunshi
|
|
68
|
+
- [`@bomb.sh/tab`](https://github.com/bombshell-dev/tab), Powered by shell auto-completion for the JavaScript ecosystem
|
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
# Plugin Decorators
|
|
2
|
+
|
|
3
|
+
Decorators are a powerful mechanism in Gunshi's plugin system that allows you to wrap and enhance existing functionality.
|
|
4
|
+
|
|
5
|
+
This guide explains how to effectively use decorators in your plugins.
|
|
6
|
+
|
|
7
|
+
## Understanding Decorator Mechanism
|
|
8
|
+
|
|
9
|
+
In Gunshi, decorators create a wrapping structure around the original functionality.
|
|
10
|
+
|
|
11
|
+
Gunshi implements two types of decorators with different processing methods:
|
|
12
|
+
|
|
13
|
+
- **Command Decorators**: Processed using `reduceRight`, creating a nested wrapper structure
|
|
14
|
+
- **Renderer Decorators**: Processed using a `for` loop, building a chain of transformations
|
|
15
|
+
|
|
16
|
+
## Command Decorators
|
|
17
|
+
|
|
18
|
+
Command decorators wrap command execution for cross-cutting concerns like logging, authentication, and error handling.
|
|
19
|
+
|
|
20
|
+
Unlike renderer decorators that only affect output formatting, command decorators can control the entire execution flow, including validation, authentication, logging, and error handling.
|
|
21
|
+
|
|
22
|
+
### How Command Decorators Work
|
|
23
|
+
|
|
24
|
+
Command decorators use the `decorateCommand()` method provided by the `PluginContext`.
|
|
25
|
+
|
|
26
|
+
Each decorator receives a runner function (the next decorator or original command) and returns a new function that wraps it:
|
|
27
|
+
|
|
28
|
+
```js
|
|
29
|
+
ctx.decorateCommand(runner => async ctx => {
|
|
30
|
+
// Pre-execution logic
|
|
31
|
+
console.log('Before command')
|
|
32
|
+
|
|
33
|
+
// Call the next decorator or original command
|
|
34
|
+
const result = await runner(ctx)
|
|
35
|
+
|
|
36
|
+
// Post-execution logic
|
|
37
|
+
console.log('After command')
|
|
38
|
+
|
|
39
|
+
return result
|
|
40
|
+
})
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Command Decorator Execution Order
|
|
44
|
+
|
|
45
|
+
Gunshi applies command decorators using the `reduceRight` method, which processes the decorator array from the last element to the first.
|
|
46
|
+
|
|
47
|
+
This approach creates a nested wrapper structure where the first registered decorator becomes the outermost layer.
|
|
48
|
+
|
|
49
|
+
The following diagram illustrates the wrapper structure:
|
|
50
|
+
|
|
51
|
+
```mermaid
|
|
52
|
+
graph TB
|
|
53
|
+
A["User Input"] --> B["Decorator A<br/>First registered - Outermost wrapper<br/> "]
|
|
54
|
+
B --> C["Decorator B<br/>Second registered"]
|
|
55
|
+
C --> D["Decorator C<br/>Last registered - Innermost wrapper<br/> "]
|
|
56
|
+
D --> E["Original Command"]
|
|
57
|
+
E --> F["Result"]
|
|
58
|
+
|
|
59
|
+
style B fill:#9B59B6,stroke:#633974,stroke-width:2px,color:#fff
|
|
60
|
+
style C fill:#4A90E2,stroke:#2E5A8E,stroke-width:2px,color:#fff
|
|
61
|
+
style D fill:#468c56,stroke:#2e5936,stroke-width:2px,color:#fff
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Basic Command Decorator Example
|
|
65
|
+
|
|
66
|
+
The following example demonstrates the execution order when using `reduceRight`:
|
|
67
|
+
|
|
68
|
+
```js [plugin.js]
|
|
69
|
+
import { plugin } from 'gunshi/plugin'
|
|
70
|
+
|
|
71
|
+
export default plugin({
|
|
72
|
+
id: 'my-plugin',
|
|
73
|
+
setup(ctx) {
|
|
74
|
+
// Registered first
|
|
75
|
+
ctx.decorateCommand(runner => async ctx => {
|
|
76
|
+
console.log('Decorator A: before')
|
|
77
|
+
const result = await runner(ctx)
|
|
78
|
+
console.log('Decorator A: after')
|
|
79
|
+
return result
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
// Registered second
|
|
83
|
+
ctx.decorateCommand(runner => async ctx => {
|
|
84
|
+
console.log('Decorator B: before')
|
|
85
|
+
const result = await runner(ctx)
|
|
86
|
+
console.log('Decorator B: after')
|
|
87
|
+
return result
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
// Registered third (executes first!)
|
|
91
|
+
ctx.decorateCommand(runner => async ctx => {
|
|
92
|
+
console.log('Decorator C: before')
|
|
93
|
+
const result = await runner(ctx)
|
|
94
|
+
console.log('Decorator C: after')
|
|
95
|
+
return result
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
})
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Application codes:
|
|
102
|
+
|
|
103
|
+
```js [cli.js]
|
|
104
|
+
import { cli } from 'gunshi'
|
|
105
|
+
import lifo from './plugin.js'
|
|
106
|
+
|
|
107
|
+
await cli(
|
|
108
|
+
process.argv.slice(2),
|
|
109
|
+
() => {
|
|
110
|
+
console.log('Original command execution')
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
plugins: [lifo]
|
|
114
|
+
}
|
|
115
|
+
)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
<!-- eslint-disable markdown/no-missing-label-refs -->
|
|
119
|
+
|
|
120
|
+
> [!TIP]
|
|
121
|
+
> The example fully code is [here](https://github.com/kazupon/gunshi/tree/main/playground/plugins/decorators/lifo).
|
|
122
|
+
|
|
123
|
+
<!-- eslint-enable markdown/no-missing-label-refs -->
|
|
124
|
+
|
|
125
|
+
When executed, `reduceRight` creates a wrapper structure where Decorator A wraps B, B wraps C, and C wraps the original command:
|
|
126
|
+
|
|
127
|
+
```sh
|
|
128
|
+
node cli.js
|
|
129
|
+
Decorator A: before # Outermost wrapper executes first
|
|
130
|
+
Decorator B: before # Middle wrapper
|
|
131
|
+
Decorator C: before # Innermost wrapper
|
|
132
|
+
Original command execution
|
|
133
|
+
Decorator C: after # Innermost completes first
|
|
134
|
+
Decorator B: after # Middle completes
|
|
135
|
+
Decorator A: after # Outermost completes last
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Advanced Command Decorator Example
|
|
139
|
+
|
|
140
|
+
Here's a complete example demonstrating how multiple command decorators work together for different purposes:
|
|
141
|
+
|
|
142
|
+
```js [plugin.js]
|
|
143
|
+
import { plugin } from 'gunshi/plugin'
|
|
144
|
+
|
|
145
|
+
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
|
|
146
|
+
|
|
147
|
+
export default plugin({
|
|
148
|
+
id: 'multi-decorator',
|
|
149
|
+
setup(ctx) {
|
|
150
|
+
// First decorator: Logging
|
|
151
|
+
ctx.decorateCommand(runner => async ctx => {
|
|
152
|
+
console.log('[LOG] Command started:', ctx.name)
|
|
153
|
+
const result = await runner(ctx)
|
|
154
|
+
console.log('[LOG] Command completed')
|
|
155
|
+
return result
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
// Second decorator: Timing
|
|
159
|
+
ctx.decorateCommand(runner => async ctx => {
|
|
160
|
+
const start = Date.now()
|
|
161
|
+
await sleep(10)
|
|
162
|
+
const result = await runner(ctx)
|
|
163
|
+
console.log(`[TIME] Execution: ${Date.now() - start}ms`)
|
|
164
|
+
return result
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
// Third decorator: Error wrapper
|
|
168
|
+
ctx.decorateCommand(runner => async ctx => {
|
|
169
|
+
try {
|
|
170
|
+
console.log('[ERROR] Monitoring enabled')
|
|
171
|
+
return await runner(ctx)
|
|
172
|
+
} catch (error) {
|
|
173
|
+
console.error('[ERROR] Command failed:', error.message)
|
|
174
|
+
throw error
|
|
175
|
+
}
|
|
176
|
+
})
|
|
177
|
+
}
|
|
178
|
+
})
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
```js [cli.js]
|
|
182
|
+
import { cli, define } from 'gunshi'
|
|
183
|
+
import multi from './plugin.js'
|
|
184
|
+
|
|
185
|
+
const command = define({
|
|
186
|
+
name: 'process',
|
|
187
|
+
run: ctx => {
|
|
188
|
+
console.log('>>> Executing actual command <<<')
|
|
189
|
+
return 'Command result'
|
|
190
|
+
}
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
await cli(process.argv.slice(2), command, {
|
|
194
|
+
plugins: [multi]
|
|
195
|
+
})
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
<!-- eslint-disable markdown/no-missing-label-refs -->
|
|
199
|
+
|
|
200
|
+
> [!TIP]
|
|
201
|
+
> The example fully code is [here](https://github.com/kazupon/gunshi/tree/main/playground/plugins/decorators/command).
|
|
202
|
+
|
|
203
|
+
<!-- eslint-enable markdown/no-missing-label-refs -->
|
|
204
|
+
|
|
205
|
+
Running `node cli.js` outputs:
|
|
206
|
+
|
|
207
|
+
```sh
|
|
208
|
+
[LOG] Command started: process
|
|
209
|
+
[ERROR] Monitoring enabled
|
|
210
|
+
>>> Executing actual command <<<
|
|
211
|
+
[TIME] Execution: 11ms
|
|
212
|
+
[LOG] Command completed
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
<!-- eslint-disable markdown/no-missing-label-refs -->
|
|
216
|
+
|
|
217
|
+
> [!NOTE]
|
|
218
|
+
> The `@gunshi/plugin-global` plugin uses a command decorator to intercept `--help` and `--version` options, preventing normal command execution and triggering rendering instead.
|
|
219
|
+
|
|
220
|
+
<!-- eslint-enable markdown/no-missing-label-refs -->
|
|
221
|
+
|
|
222
|
+
## Renderer Decorators
|
|
223
|
+
|
|
224
|
+
Gunshi provides a powerful API for customizing how your CLI displays information through renderer decorators.
|
|
225
|
+
|
|
226
|
+
These decorators allow you to wrap and enhance the rendering of headers, usage/help messages, and validation errors, enabling consistent styling, branding, and enhanced user experience across your CLI application.
|
|
227
|
+
|
|
228
|
+
### How Renderer Decorators Work
|
|
229
|
+
|
|
230
|
+
Gunshi applies renderer decorators using a standard `for` loop that iterates through the decorator array from first to last.
|
|
231
|
+
|
|
232
|
+
Each iteration wraps the previous renderer function, building a chain of decorators.
|
|
233
|
+
|
|
234
|
+
This approach means that each decorator in the array wraps the accumulated result of all previous decorators, with each decorator receiving the previous renderer as its `baseRenderer` parameter.
|
|
235
|
+
|
|
236
|
+
### Available Renderer Decorator Methods
|
|
237
|
+
|
|
238
|
+
Gunshi provides three renderer decorator methods via `PluginContext`:
|
|
239
|
+
|
|
240
|
+
- **`decorateHeaderRenderer`**: Customizes command headers (title/branding)
|
|
241
|
+
- **`decorateUsageRenderer`**: Enhances usage and help message display
|
|
242
|
+
- **`decorateValidationErrorsRenderer`**: Formats validation error messages
|
|
243
|
+
|
|
244
|
+
Each decorator receives the base renderer function and must call it to maintain the decorator chain.
|
|
245
|
+
|
|
246
|
+
This ensures that multiple plugins can cooperatively enhance the output.
|
|
247
|
+
|
|
248
|
+
### Complete Rendering Customization Example
|
|
249
|
+
|
|
250
|
+
Here's a comprehensive example showing how to customize all three renderers in a single plugin.
|
|
251
|
+
|
|
252
|
+
This plugin adds branding to headers, appends metadata to usage messages, and enhances error formatting:
|
|
253
|
+
|
|
254
|
+
```js [plugin.js]
|
|
255
|
+
import { plugin } from 'gunshi/plugin'
|
|
256
|
+
|
|
257
|
+
export default plugin({
|
|
258
|
+
id: 'custom-renderer',
|
|
259
|
+
setup(ctx) {
|
|
260
|
+
// Add branding to header
|
|
261
|
+
ctx.decorateHeaderRenderer(async (baseRenderer, ctx) => {
|
|
262
|
+
const header = await baseRenderer(ctx)
|
|
263
|
+
return `š My CLI v${ctx.env.version}\n${header}`
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
// Append timestamp to usage
|
|
267
|
+
ctx.decorateUsageRenderer(async (baseRenderer, ctx) => {
|
|
268
|
+
const usage = await baseRenderer(ctx)
|
|
269
|
+
return `${usage}\n\nGenerated: ${new Date().toISOString()}`
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
// Format validation errors with emoji
|
|
273
|
+
ctx.decorateValidationErrorsRenderer(async (baseRenderer, ctx, error) => {
|
|
274
|
+
const errors = await baseRenderer(ctx, error)
|
|
275
|
+
return `ā Validation Error:\n${errors}`
|
|
276
|
+
})
|
|
277
|
+
}
|
|
278
|
+
})
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
Application code:
|
|
282
|
+
|
|
283
|
+
```js [cli.js]
|
|
284
|
+
import { cli, define } from 'gunshi'
|
|
285
|
+
import customRenderer from './plugin.js'
|
|
286
|
+
|
|
287
|
+
await cli(
|
|
288
|
+
process.argv.slice(2),
|
|
289
|
+
define({
|
|
290
|
+
name: 'build',
|
|
291
|
+
args: {
|
|
292
|
+
output: { type: 'string', required: true }
|
|
293
|
+
},
|
|
294
|
+
run: ctx => console.log(`Building to ${ctx.values.output}`)
|
|
295
|
+
}),
|
|
296
|
+
{
|
|
297
|
+
name: 'my-cli',
|
|
298
|
+
version: '1.0.0',
|
|
299
|
+
plugins: [customRenderer]
|
|
300
|
+
}
|
|
301
|
+
)
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
<!-- eslint-disable markdown/no-missing-label-refs -->
|
|
305
|
+
|
|
306
|
+
> [!TIP]
|
|
307
|
+
> The example fully code is [here](https://github.com/kazupon/gunshi/tree/main/playground/plugins/decorators/renderers).
|
|
308
|
+
|
|
309
|
+
<!-- eslint-enable markdown/no-missing-label-refs -->
|
|
310
|
+
|
|
311
|
+
Run with `--help` to see customized output:
|
|
312
|
+
|
|
313
|
+
```sh
|
|
314
|
+
node cli.js --help
|
|
315
|
+
š My CLI v1.0.0
|
|
316
|
+
my-cli (my-cli v1.0.0)
|
|
317
|
+
|
|
318
|
+
USAGE:
|
|
319
|
+
my-cli <OPTIONS>
|
|
320
|
+
|
|
321
|
+
OPTIONS:
|
|
322
|
+
-h, --help Display this help message
|
|
323
|
+
-v, --version Display this version
|
|
324
|
+
--output <output>
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
Generated: 2025-08-15T14:26:43.121Z
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
### Multiple Plugin Decorator Execution Order
|
|
331
|
+
|
|
332
|
+
When multiple plugins register renderer decorators, the order matters.
|
|
333
|
+
|
|
334
|
+
Gunshi uses two built-in plugins by default: `@gunshi/plugin-global` (adds `--help` and `--version` options) and `@gunshi/plugin-renderer` (provides default rendering).
|
|
335
|
+
|
|
336
|
+
When you add your own plugins, they interact with these default plugins in a specific order based on how the `for` loop processes the decorators.
|
|
337
|
+
|
|
338
|
+
#### Plugin Registration and Decorator Chain Building
|
|
339
|
+
|
|
340
|
+
The following diagram shows how plugins are registered and how the `for` loop builds the decorator chain:
|
|
341
|
+
|
|
342
|
+
<h5 style="text-align: center; padding: 1em; margin: 1em;">Plugin Registration Order</h5>
|
|
343
|
+
|
|
344
|
+
```mermaid
|
|
345
|
+
graph TB
|
|
346
|
+
R1["1. plugin-global<br/>Command Decorator"]
|
|
347
|
+
R2["2. plugin-renderer<br/>Renderer Decorators"]
|
|
348
|
+
R3["3. custom-plugin-A<br/>Renderer Decorator"]
|
|
349
|
+
R4["4. custom-plugin-B<br/>Renderer Decorator"]
|
|
350
|
+
R1 --> R2
|
|
351
|
+
R2 --> R3
|
|
352
|
+
R3 --> R4
|
|
353
|
+
|
|
354
|
+
style R1 fill:#9B59B6,stroke:#633974,stroke-width:2px,color:#fff
|
|
355
|
+
style R2 fill:#4A90E2,stroke:#2E5A8E,stroke-width:2px,color:#fff
|
|
356
|
+
style R3 fill:#E67E22,stroke:#A35D18,stroke-width:2px,color:#fff
|
|
357
|
+
style R4 fill:#E67E22,stroke:#A35D18,stroke-width:2px,color:#fff
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
<h5 style="text-align: center; padding: 1em; margin: 1em">Renderer Decorator Chain (for loop builds)</h5>
|
|
361
|
+
|
|
362
|
+
```mermaid
|
|
363
|
+
graph TB
|
|
364
|
+
E1["Base Renderer<br/>empty string<br/> "]
|
|
365
|
+
E2["plugin-renderer wraps base"]
|
|
366
|
+
E3["custom-A wraps plugin-renderer"]
|
|
367
|
+
E4["custom-B wraps custom-A"]
|
|
368
|
+
E1 --> E2
|
|
369
|
+
E2 --> E3
|
|
370
|
+
E3 --> E4
|
|
371
|
+
|
|
372
|
+
style E1 fill:#468c56,stroke:#2e5936,stroke-width:2px,color:#fff
|
|
373
|
+
style E2 fill:#4A90E2,stroke:#2E5A8E,stroke-width:2px,color:#fff
|
|
374
|
+
style E3 fill:#E67E22,stroke:#A35D18,stroke-width:2px,color:#fff
|
|
375
|
+
style E4 fill:#E67E22,stroke:#A35D18,stroke-width:2px,color:#fff
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
#### How Default and Custom Plugins Interact
|
|
379
|
+
|
|
380
|
+
Here's an example showing how the default Gunshi plugins work together with custom plugins:
|
|
381
|
+
|
|
382
|
+
custom-plugin-A:
|
|
383
|
+
|
|
384
|
+
```js [plugin-a.js]
|
|
385
|
+
import { plugin } from 'gunshi/plugin'
|
|
386
|
+
|
|
387
|
+
export default plugin({
|
|
388
|
+
id: 'custom-a',
|
|
389
|
+
setup(ctx) {
|
|
390
|
+
ctx.decorateUsageRenderer(async (baseRenderer, ctx) => {
|
|
391
|
+
const usage = await baseRenderer(ctx) // Call next decorator first
|
|
392
|
+
console.log('[custom-a] Decorating usage')
|
|
393
|
+
return `${usage}\nš¦ Enhanced by Plugin A`
|
|
394
|
+
})
|
|
395
|
+
}
|
|
396
|
+
})
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
custom-plugin-B:
|
|
400
|
+
|
|
401
|
+
```js [plugin-b.js]
|
|
402
|
+
import { plugin } from 'gunshi/plugin'
|
|
403
|
+
|
|
404
|
+
export default plugin({
|
|
405
|
+
id: 'custom-b',
|
|
406
|
+
setup(ctx) {
|
|
407
|
+
ctx.decorateUsageRenderer(async (baseRenderer, ctx) => {
|
|
408
|
+
const usage = await baseRenderer(ctx) // Call next decorator first
|
|
409
|
+
console.log('[custom-b] Decorating usage')
|
|
410
|
+
return `${usage}\nšØ Styled by Plugin B`
|
|
411
|
+
})
|
|
412
|
+
}
|
|
413
|
+
})
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
Last, install all plugins on CLI application:
|
|
417
|
+
|
|
418
|
+
```js [cli.js]
|
|
419
|
+
import { cli, define } from 'gunshi' // Includes plugin-global and plugin-renderer by default
|
|
420
|
+
import pluginA from './plugin-a.js'
|
|
421
|
+
import pluginB from './plugin-b.js'
|
|
422
|
+
|
|
423
|
+
await cli(
|
|
424
|
+
process.argv.slice(2),
|
|
425
|
+
define({
|
|
426
|
+
name: 'demo',
|
|
427
|
+
run: () => console.log('Demo command')
|
|
428
|
+
}),
|
|
429
|
+
{
|
|
430
|
+
name: 'my-cli',
|
|
431
|
+
version: '1.0.0',
|
|
432
|
+
renderHeader: null, // Disable default header rendering
|
|
433
|
+
// Custom plugins are added after default plugins
|
|
434
|
+
plugins: [pluginA, pluginB]
|
|
435
|
+
}
|
|
436
|
+
)
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
<!-- eslint-disable markdown/no-missing-label-refs -->
|
|
440
|
+
|
|
441
|
+
> [!TIP]
|
|
442
|
+
> The example fully code is [here](https://github.com/kazupon/gunshi/tree/main/playground/plugins/decorators/multiple-order).
|
|
443
|
+
|
|
444
|
+
<!-- eslint-enable markdown/no-missing-label-refs -->
|
|
445
|
+
|
|
446
|
+
#### Execution Flow Breakdown
|
|
447
|
+
|
|
448
|
+
When you run `node index.js --help`, two different types of decorators work together:
|
|
449
|
+
|
|
450
|
+
**1. Command Decorator (`@gunshi/plugin-global`):**
|
|
451
|
+
|
|
452
|
+
- Intercepts the `--help` option
|
|
453
|
+
- Calls the renderer functions to generate output
|
|
454
|
+
|
|
455
|
+
**2. Renderer Decorators (chain built by for loop):**
|
|
456
|
+
|
|
457
|
+
The `for` loop builds a chain where:
|
|
458
|
+
|
|
459
|
+
- custom-plugin-B wraps custom-plugin-A
|
|
460
|
+
- custom-plugin-A wraps plugin-renderer
|
|
461
|
+
- plugin-renderer wraps the base renderer (empty string)
|
|
462
|
+
|
|
463
|
+
**Execution flow when each decorator calls `baseRenderer` first:**
|
|
464
|
+
|
|
465
|
+
1. custom-plugin-B decorator starts ā calls `baseRenderer`
|
|
466
|
+
2. custom-plugin-A decorator starts ā calls `baseRenderer`
|
|
467
|
+
3. plugin-renderer decorator executes ā returns full usage
|
|
468
|
+
4. custom-plugin-A continues ā logs and adds "š¦ Enhanced by Plugin A"
|
|
469
|
+
5. custom-plugin-B continues ā logs and adds "šØ Styled by Plugin B"
|
|
470
|
+
|
|
471
|
+
The console output in this example:
|
|
472
|
+
|
|
473
|
+
```sh
|
|
474
|
+
[custom-a] Decorating usage // Logs after its baseRenderer returns
|
|
475
|
+
[custom-b] Decorating usage // Logs after its baseRenderer returns
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
And the final rendered output:
|
|
479
|
+
|
|
480
|
+
```sh
|
|
481
|
+
USAGE:
|
|
482
|
+
my-cli <OPTIONS>
|
|
483
|
+
|
|
484
|
+
OPTIONS:
|
|
485
|
+
-h, --help Display this help message
|
|
486
|
+
-v, --version Display this version
|
|
487
|
+
|
|
488
|
+
š¦ Enhanced by Plugin A
|
|
489
|
+
šØ Styled by Plugin B
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
#### Understanding the Chain
|
|
493
|
+
|
|
494
|
+
The renderer decorator chain works differently than you might expect:
|
|
495
|
+
|
|
496
|
+
```js
|
|
497
|
+
// Actual execution flow for renderer decorators
|
|
498
|
+
const base = await baseRenderer(ctx) // Returns ""
|
|
499
|
+
const afterRenderer = await rendererDecorator(base, ctx) // Doesn't call base, returns full usage
|
|
500
|
+
const afterCustomA = await customADecorator(afterRenderer, ctx) // Adds "Enhanced by Plugin A"
|
|
501
|
+
const final = await customBDecorator(afterCustomA, ctx) // Adds "Styled by Plugin B"
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
<!-- eslint-disable markdown/no-missing-label-refs -->
|
|
505
|
+
|
|
506
|
+
> [!NOTE]
|
|
507
|
+
> `@gunshi/plugin-global` uses a **command decorator** to handle `--help`/`--version` options, while `@gunshi/plugin-renderer` uses **renderer decorators** to format the output. The base renderer returns an empty string, and `@gunshi/plugin-renderer` provides the actual implementation.
|
|
508
|
+
|
|
509
|
+
<!-- eslint-enable markdown/no-missing-label-refs -->
|
|
510
|
+
|
|
511
|
+
<!-- eslint-disable markdown/no-missing-label-refs -->
|
|
512
|
+
|
|
513
|
+
> [!IMPORTANT]
|
|
514
|
+
> Always call `baseRenderer` in your decorator to maintain the decorator chain. While `@gunshi/plugin-renderer` replaces the empty base renderer with full implementation, your custom decorators should enhance the output from previous decorators in the chain.
|
|
515
|
+
|
|
516
|
+
<!-- eslint-enable markdown/no-missing-label-refs -->
|
|
517
|
+
|
|
518
|
+
### Important Considerations
|
|
519
|
+
|
|
520
|
+
**Always call `baseRenderer` in your decorator to maintain the decorator chain. Skipping it will break other plugins that may depend on the output.**
|
|
521
|
+
|
|
522
|
+
<!-- eslint-disable markdown/no-missing-label-refs -->
|
|
523
|
+
|
|
524
|
+
> [!NOTE]
|
|
525
|
+
> Renderer decorators have the lowest priority in Gunshi's rendering system. Command-level and CLI-level renderers will override plugin decorators. See [Rendering Customization](../advanced/custom-rendering.md) for details on renderer priority.
|
|
526
|
+
|
|
527
|
+
<!-- eslint-enable markdown/no-missing-label-refs -->
|
|
528
|
+
|
|
529
|
+
## Command vs Renderer Decorators
|
|
530
|
+
|
|
531
|
+
Understanding the difference between these two decorator types is crucial:
|
|
532
|
+
|
|
533
|
+
| Aspect | Command Decorator | Renderer Decorator |
|
|
534
|
+
| -------------- | ---------------------------------- | --------------------------------------- |
|
|
535
|
+
| **Purpose** | Wraps command execution | Wraps output rendering |
|
|
536
|
+
| **Method** | `ctx.decorateCommand()` | `ctx.decorateUsageRenderer()`, etc. |
|
|
537
|
+
| **Can modify** | Command behavior, flow control | Output formatting only |
|
|
538
|
+
| **Can access** | Full CommandContext | CommandContext + render-specific params |
|
|
539
|
+
| **Use cases** | Auth, logging, validation, caching | Styling, i18n, branding |
|
|
540
|
+
|
|
541
|
+
## Next Steps
|
|
542
|
+
|
|
543
|
+
With decorators, you've learned how to wrap and enhance command behavior and rendering output. This mechanism enables cross-cutting concerns like authentication, logging, and custom formatting without modifying command implementations.
|
|
544
|
+
|
|
545
|
+
The next chapter, [Plugin Extensions](./extensions.md), will show you how plugins can share functionality with commands through context extensions, creating a communication channel between plugins and the rest of your CLI application.
|