@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,561 @@
|
|
|
1
|
+
# Type System
|
|
2
|
+
|
|
3
|
+
Gunshi v0.27 introduces a powerful type parameter system that provides comprehensive type safety across all core functions: `cli`, `define`, `lazy`, and `plugin`.
|
|
4
|
+
|
|
5
|
+
This enhancement brings TypeScript's full type-checking capabilities to your CLI applications, ensuring compile-time safety for command arguments and plugin extensions.
|
|
6
|
+
|
|
7
|
+
This guide focuses on type safety for command definitions and their arguments. If you're creating custom plugins and need to understand the `plugin` function's type system, refer to the [Plugin Type System](../plugin/type-system.md) guide.
|
|
8
|
+
|
|
9
|
+
<!-- eslint-disable markdown/no-missing-label-refs -->
|
|
10
|
+
|
|
11
|
+
> [!NOTE]
|
|
12
|
+
> Some code examples in this guide include TypeScript file extensions (`.ts`) in `import`/`export` statements. If you use this pattern in your plugin, you'll need to enable `allowImportingTsExtensions` in your `tsconfig.json`.
|
|
13
|
+
|
|
14
|
+
<!-- eslint-enable markdown/no-missing-label-refs -->
|
|
15
|
+
|
|
16
|
+
## Overview of v0.27 Type System
|
|
17
|
+
|
|
18
|
+
The v0.27 release provides comprehensive type safety through:
|
|
19
|
+
|
|
20
|
+
- **Core Functions**: `define` and `lazy` provide excellent type inference for standard commands
|
|
21
|
+
- **Plugin Extension Support**: `defineWithTypes` and `lazyWithTypes` enable type declarations for plugin extensions
|
|
22
|
+
- **Unified Type System (GunshiParams)**: A coherent type system for arguments and extensions
|
|
23
|
+
- **CLI Entry Point Types**: Type parameters for the `cli` function's entry command
|
|
24
|
+
- **Enhanced TypeScript Inference**: Automatic type inference reduces boilerplate while maintaining safety
|
|
25
|
+
|
|
26
|
+
## Understanding `GunshiParams`
|
|
27
|
+
|
|
28
|
+
`GunshiParams` is the core type that provides type safety for command arguments and plugin extensions. At its simplest, it ensures that your command context has properly typed `values` (from args) and `extensions` (from plugins).
|
|
29
|
+
|
|
30
|
+
Before diving into the implementation details, let's start with a simple example that demonstrates how `GunshiParams` provides type safety for command arguments:
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
// Simple usage - just arguments
|
|
34
|
+
type SimpleParams = GunshiParams<{
|
|
35
|
+
args: { port: { type: 'number' } }
|
|
36
|
+
}>
|
|
37
|
+
|
|
38
|
+
// With extensions from plugins
|
|
39
|
+
type FullParams = GunshiParams<{
|
|
40
|
+
args: { port: { type: 'number' } }
|
|
41
|
+
extensions: { logger: Logger }
|
|
42
|
+
}>
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The actual type definition uses TypeScript's conditional types to provide flexibility:
|
|
46
|
+
|
|
47
|
+
<<< ../../../../gunshi/src/types.ts#snippet
|
|
48
|
+
|
|
49
|
+
## Core Functions: `define` and `lazy`
|
|
50
|
+
|
|
51
|
+
### The `define` Function
|
|
52
|
+
|
|
53
|
+
The `define` function is the primary way to create commands in Gunshi. For most use cases, the basic usage with automatic type inference is sufficient. Advanced usage with type parameters is useful when you need explicit type control or are working with complex type definitions.
|
|
54
|
+
|
|
55
|
+
#### Basic Usage
|
|
56
|
+
|
|
57
|
+
When your command doesn't use plugin extensions, `define` provides excellent automatic type inference:
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
import { define } from 'gunshi'
|
|
61
|
+
|
|
62
|
+
// Standard command definition with automatic type inference
|
|
63
|
+
export const serverCommand = define({
|
|
64
|
+
name: 'server',
|
|
65
|
+
description: 'Start the development server',
|
|
66
|
+
args: {
|
|
67
|
+
port: { type: 'number', default: 3000 },
|
|
68
|
+
host: { type: 'string', default: 'localhost' },
|
|
69
|
+
verbose: { type: 'boolean', short: 'V' }
|
|
70
|
+
},
|
|
71
|
+
run: ctx => {
|
|
72
|
+
// ctx.values is automatically inferred as { port?: number; host?: string; verbose?: boolean }
|
|
73
|
+
const { port, host, verbose } = ctx.values
|
|
74
|
+
|
|
75
|
+
console.log(`Starting server on ${host}:${port}`)
|
|
76
|
+
if (verbose) {
|
|
77
|
+
console.log('Verbose mode enabled')
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
The basic usage covers most scenarios where you're defining commands with inline arguments and don't need to share type definitions across multiple commands.
|
|
84
|
+
|
|
85
|
+
#### Advanced Usage with Type Parameters
|
|
86
|
+
|
|
87
|
+
When you need explicit type control or are working with pre-defined argument configurations, `define` accepts `GunshiParams` compatible type parameter:
|
|
88
|
+
|
|
89
|
+
```ts [commands/server.ts]
|
|
90
|
+
import { define } from 'gunshi'
|
|
91
|
+
|
|
92
|
+
// Define args separately
|
|
93
|
+
export const serverArgs = {
|
|
94
|
+
port: { type: 'number', default: 3000 },
|
|
95
|
+
host: { type: 'string', default: 'localhost' }
|
|
96
|
+
} as const
|
|
97
|
+
|
|
98
|
+
export const serverCommand = define<{ args: typeof serverArgs }>({
|
|
99
|
+
name: 'server',
|
|
100
|
+
args: serverArgs,
|
|
101
|
+
run: ctx => {
|
|
102
|
+
// ctx.values is typed based on ServerParams
|
|
103
|
+
console.log(`Server: ${ctx.values.host}:${ctx.values.port}`)
|
|
104
|
+
}
|
|
105
|
+
})
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
<!-- eslint-disable markdown/no-missing-label-refs -->
|
|
109
|
+
|
|
110
|
+
> [!TIP]
|
|
111
|
+
> The example fully code is [here](https://github.com/kazupon/gunshi/tree/main/playground/advanced/type-system/define).
|
|
112
|
+
|
|
113
|
+
<!-- eslint-enable markdown/no-missing-label-refs -->
|
|
114
|
+
|
|
115
|
+
This advanced approach is particularly useful when you want to reuse argument definitions across multiple commands or need to export types for use in other modules.
|
|
116
|
+
|
|
117
|
+
### The `lazy` Function
|
|
118
|
+
|
|
119
|
+
The `lazy` function enables code-splitting while maintaining type safety. Like `define`, it works well with automatic type inference for most cases. Advanced usage provides explicit type control when needed.
|
|
120
|
+
|
|
121
|
+
#### Basic Usage
|
|
122
|
+
|
|
123
|
+
For commands without plugin extensions, `lazy` automatically infers types from your definition:
|
|
124
|
+
|
|
125
|
+
```js
|
|
126
|
+
import { lazy } from 'gunshi'
|
|
127
|
+
|
|
128
|
+
// Lazy-loaded command with automatic type inference
|
|
129
|
+
export const buildCommand = lazy(
|
|
130
|
+
async () => {
|
|
131
|
+
// Heavy dependencies can be loaded here when needed
|
|
132
|
+
const { build } = await import('./build.js')
|
|
133
|
+
|
|
134
|
+
return async ctx => {
|
|
135
|
+
// ctx.values is automatically inferred from args definition below
|
|
136
|
+
const { target, minify } = ctx.values
|
|
137
|
+
|
|
138
|
+
console.log(`Building for ${target}...`)
|
|
139
|
+
if (minify) {
|
|
140
|
+
console.log('Minification enabled')
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return build({ target, minify })
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
name: 'build',
|
|
148
|
+
description: 'Build the project',
|
|
149
|
+
args: {
|
|
150
|
+
target: { type: 'string', required: true, choices: ['dev', 'prod'] },
|
|
151
|
+
minify: { type: 'boolean', default: false }
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
)
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
This basic usage is ideal for most lazy-loaded commands where you want to defer loading dependencies until the command is actually executed.
|
|
158
|
+
|
|
159
|
+
#### Advanced Usage with Pre-defined Arguments
|
|
160
|
+
|
|
161
|
+
When working with pre-defined argument configurations or when you need to explicitly type your command runner, you can structure your lazy command as follows:
|
|
162
|
+
|
|
163
|
+
```ts [commands/build.ts]
|
|
164
|
+
import { lazy } from 'gunshi'
|
|
165
|
+
|
|
166
|
+
// Pre-defined arguments
|
|
167
|
+
export const buildArgs = {
|
|
168
|
+
target: { type: 'enum', required: true, choices: ['dev', 'prod'] },
|
|
169
|
+
minify: { type: 'boolean', default: false }
|
|
170
|
+
} as const
|
|
171
|
+
|
|
172
|
+
// Create the lazy command with explicit typing
|
|
173
|
+
export const buildCommand = lazy<{ args: typeof buildArgs }>(
|
|
174
|
+
async () => {
|
|
175
|
+
// Heavy dependencies can be loaded here when needed
|
|
176
|
+
const { bundle } = await import('./utils.ts')
|
|
177
|
+
|
|
178
|
+
return async ctx => {
|
|
179
|
+
// Inference of args values
|
|
180
|
+
const { target, minify } = ctx.values
|
|
181
|
+
// Implementation
|
|
182
|
+
return bundle({ target, minify })
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
name: 'build',
|
|
187
|
+
args: buildArgs
|
|
188
|
+
}
|
|
189
|
+
)
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
<!-- eslint-disable markdown/no-missing-label-refs -->
|
|
193
|
+
|
|
194
|
+
> [!TIP]
|
|
195
|
+
> The example fully code is [here](https://github.com/kazupon/gunshi/tree/main/playground/advanced/type-system/lazy).
|
|
196
|
+
|
|
197
|
+
<!-- eslint-enable markdown/no-missing-label-refs -->
|
|
198
|
+
|
|
199
|
+
## Plugin Extensions and Architectural Constraints
|
|
200
|
+
|
|
201
|
+
### Understanding the Timing Constraint
|
|
202
|
+
|
|
203
|
+
Gunshi's architecture intentionally separates command definition from plugin installation:
|
|
204
|
+
|
|
205
|
+
1. **Command Definition Time**: When you call `define()` or `lazy()` in your code, plugins haven't been installed yet
|
|
206
|
+
2. **CLI Execution Time**: When `cli()` runs, plugins are installed and extensions become available
|
|
207
|
+
3. **The Gap**: Commands can't know what extensions will be available at definition time
|
|
208
|
+
|
|
209
|
+
This architectural design enables flexible command configuration but creates a challenge: how can commands safely use plugin extensions that don't exist yet?
|
|
210
|
+
|
|
211
|
+
### The Solution: Type Declaration Functions
|
|
212
|
+
|
|
213
|
+
`defineWithTypes` and `lazyWithTypes` solve this by allowing you to declare expected extensions at command definition time. These functions provide type safety for plugin extensions that will be available when the command actually runs.
|
|
214
|
+
|
|
215
|
+
This separation is crucial because:
|
|
216
|
+
|
|
217
|
+
- It maintains modularity - commands don't depend on specific plugin implementations
|
|
218
|
+
- It allows flexible plugin configuration - different CLI instances can use different plugins
|
|
219
|
+
- It enables type safety - TypeScript knows what extensions to expect even though they're not yet available
|
|
220
|
+
|
|
221
|
+
## Functions for Plugin Extensions: `defineWithTypes` and `lazyWithTypes`
|
|
222
|
+
|
|
223
|
+
When your command needs to use plugin extensions, use these specialized functions that allow you to declare expected extension types.
|
|
224
|
+
|
|
225
|
+
### The `defineWithTypes` Function
|
|
226
|
+
|
|
227
|
+
Use `defineWithTypes` when your command needs plugin extensions. It uses a currying approach where you specify the extensions type, and args are automatically inferred:
|
|
228
|
+
|
|
229
|
+
```ts [commands/server.ts]
|
|
230
|
+
import { defineWithTypes } from 'gunshi'
|
|
231
|
+
import type { AuthExtension } from '../plugin.ts'
|
|
232
|
+
|
|
233
|
+
// Define your extensions type
|
|
234
|
+
type ServerExtensions = {
|
|
235
|
+
auth: AuthExtension
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Use defineWithTypes - specify only extensions, args are inferred!
|
|
239
|
+
export const serverCommand = defineWithTypes<{ extensions: ServerExtensions }>()({
|
|
240
|
+
name: 'server',
|
|
241
|
+
description: 'Start the development server',
|
|
242
|
+
args: {
|
|
243
|
+
port: { type: 'number', default: 3000 },
|
|
244
|
+
host: { type: 'string', default: 'localhost' },
|
|
245
|
+
verbose: { type: 'boolean', short: 'V' }
|
|
246
|
+
},
|
|
247
|
+
run: ctx => {
|
|
248
|
+
// ctx.values is automatically inferred as { port?: number; host?: string; verbose?: boolean }
|
|
249
|
+
const { port, host, verbose } = ctx.values
|
|
250
|
+
|
|
251
|
+
// ctx.extensions is typed as ServerExtensions
|
|
252
|
+
// Optional chaining (?.) is used because plugins may not be installed
|
|
253
|
+
if (!ctx.extensions.auth?.isAuthenticated()) {
|
|
254
|
+
throw new Error('Please login first')
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const user = ctx.extensions.auth?.getUser()
|
|
258
|
+
console.log(`Server started by ${user.name} on ${host}:${port}`)
|
|
259
|
+
|
|
260
|
+
if (verbose) {
|
|
261
|
+
console.log('Verbose mode enabled')
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
})
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
<!-- eslint-disable markdown/no-missing-label-refs -->
|
|
268
|
+
|
|
269
|
+
> [!TIP]
|
|
270
|
+
> The example fully code is [here](https://github.com/kazupon/gunshi/tree/main/playground/advanced/type-system/define-with-types).
|
|
271
|
+
|
|
272
|
+
<!-- eslint-enable markdown/no-missing-label-refs -->
|
|
273
|
+
|
|
274
|
+
#### Flexible Type Parameters
|
|
275
|
+
|
|
276
|
+
`defineWithTypes` supports multiple scenarios for different use cases:
|
|
277
|
+
|
|
278
|
+
```ts
|
|
279
|
+
// Case 1: Extensions only (most common)
|
|
280
|
+
// You want to use plugin extensions in your CommandRunner implementation.
|
|
281
|
+
// Args are automatically inferred from the inline definition
|
|
282
|
+
const cmd1 = defineWithTypes<{ extensions: MyExtensions }>()({
|
|
283
|
+
name: 'server',
|
|
284
|
+
args: { port: { type: 'number' } }, // args types are inferred
|
|
285
|
+
run: ctx => {
|
|
286
|
+
// ctx.extensions is typed, ctx.values is inferred from args
|
|
287
|
+
/* ... */
|
|
288
|
+
}
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
// Case 2: Args only (when using pre-defined args)
|
|
292
|
+
// Your args are defined in a variable, not inline in the define function.
|
|
293
|
+
// Pre-defined args variables need explicit type specification for proper inference
|
|
294
|
+
const myArgs = {
|
|
295
|
+
port: { type: 'number' },
|
|
296
|
+
host: { type: 'string' }
|
|
297
|
+
} as const
|
|
298
|
+
|
|
299
|
+
const cmd2 = defineWithTypes<{ args: typeof myArgs }>()({
|
|
300
|
+
name: 'process',
|
|
301
|
+
args: myArgs, // Using pre-defined args variable
|
|
302
|
+
run: ctx => {
|
|
303
|
+
// ctx.values is typed based on myArgs
|
|
304
|
+
/* ... */
|
|
305
|
+
}
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
// Case 3: Both args and extensions (special case)
|
|
309
|
+
// You need plugin extensions AND you're using pre-defined args variables
|
|
310
|
+
const cmd3 = defineWithTypes<{
|
|
311
|
+
args: typeof myArgs
|
|
312
|
+
extensions: MyExtensions
|
|
313
|
+
}>()({
|
|
314
|
+
name: 'hybrid',
|
|
315
|
+
args: myArgs, // Using pre-defined args variable
|
|
316
|
+
run: ctx => {
|
|
317
|
+
// Both ctx.values and ctx.extensions are typed
|
|
318
|
+
/* ... */
|
|
319
|
+
}
|
|
320
|
+
})
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
### The `lazyWithTypes` Function
|
|
324
|
+
|
|
325
|
+
The `lazyWithTypes` function maintains type safety for lazy-loaded commands that use plugin extensions:
|
|
326
|
+
|
|
327
|
+
```ts [commands/build.ts]
|
|
328
|
+
import { lazyWithTypes } from 'gunshi'
|
|
329
|
+
import type { LoggerExtension } from '../plugin.ts'
|
|
330
|
+
|
|
331
|
+
export const buildArgs = {
|
|
332
|
+
target: { type: 'enum', required: true, choices: ['dev', 'prod'] },
|
|
333
|
+
minify: { type: 'boolean', default: false }
|
|
334
|
+
} as const
|
|
335
|
+
|
|
336
|
+
// Define extensions for the command
|
|
337
|
+
type BuildExtensions = {
|
|
338
|
+
logger: LoggerExtension
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Use lazyWithTypes with extensions - args are automatically inferred
|
|
342
|
+
export const buildCommand = lazyWithTypes<{
|
|
343
|
+
args: typeof buildArgs
|
|
344
|
+
extensions: BuildExtensions
|
|
345
|
+
}>()(
|
|
346
|
+
async () => {
|
|
347
|
+
// Heavy dependencies can be loaded here when needed
|
|
348
|
+
return async ctx => {
|
|
349
|
+
// ctx.values is automatically inferred from args definition below
|
|
350
|
+
const { target, minify } = ctx.values
|
|
351
|
+
|
|
352
|
+
// Use typed extensions
|
|
353
|
+
ctx.extensions.logger?.log(`Building for ${target}...`)
|
|
354
|
+
|
|
355
|
+
if (minify) {
|
|
356
|
+
ctx.extensions.logger?.log('Minification enabled')
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
name: 'build',
|
|
362
|
+
description: 'Build the project',
|
|
363
|
+
args: buildArgs
|
|
364
|
+
}
|
|
365
|
+
)
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
<!-- eslint-disable markdown/no-missing-label-refs -->
|
|
369
|
+
|
|
370
|
+
> [!TIP]
|
|
371
|
+
> The example fully code is [here](https://github.com/kazupon/gunshi/tree/main/playground/advanced/type-system/lazy-with-types).
|
|
372
|
+
|
|
373
|
+
<!-- eslint-enable markdown/no-missing-label-refs -->
|
|
374
|
+
|
|
375
|
+
#### Flexible Type Parameters
|
|
376
|
+
|
|
377
|
+
Similar to `defineWithTypes`, `lazyWithTypes` supports flexible type parameters:
|
|
378
|
+
|
|
379
|
+
<!-- eslint-skip -->
|
|
380
|
+
|
|
381
|
+
```ts
|
|
382
|
+
// Most common: specify only extensions
|
|
383
|
+
lazyWithTypes<{ extensions: BuildExtensions }>()( ... )
|
|
384
|
+
|
|
385
|
+
// When using pre-defined args: specify args explicitly
|
|
386
|
+
lazyWithTypes<{ args: typeof buildArgs }>()( ... )
|
|
387
|
+
|
|
388
|
+
// Full control: specify both (when using pre-defined args AND extensions)
|
|
389
|
+
lazyWithTypes<{ args: typeof buildArgs; extensions: BuildExtensions }>()( ... )
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
## The `cli` Function Type Parameters
|
|
393
|
+
|
|
394
|
+
The `cli` function provides type safety for your entry command. The type parameter defines the type for the entry command's context, determining the types of `ctx.values` and `ctx.extensions` that the entry command's runner function receives:
|
|
395
|
+
|
|
396
|
+
```ts
|
|
397
|
+
import { cli } from 'gunshi'
|
|
398
|
+
import logger from './plugin.ts'
|
|
399
|
+
|
|
400
|
+
import type { LoggerExtension } from './plugin.ts'
|
|
401
|
+
|
|
402
|
+
const entryArgs = {
|
|
403
|
+
verbose: { type: 'boolean', short: 'V' },
|
|
404
|
+
output: { type: 'string', default: 'json' }
|
|
405
|
+
} as const
|
|
406
|
+
|
|
407
|
+
// `cli` function with type-safe `args` and `extensions`
|
|
408
|
+
await cli<{ args: typeof entryArgs; extensions: { logger: LoggerExtension } }>(
|
|
409
|
+
process.argv.slice(2),
|
|
410
|
+
{
|
|
411
|
+
name: 'main',
|
|
412
|
+
description: 'CLI with type-safe extensions',
|
|
413
|
+
args: entryArgs,
|
|
414
|
+
run: async ctx => {
|
|
415
|
+
if (ctx.values.verbose) {
|
|
416
|
+
ctx.extensions.logger?.log(`Processing in verbose mode...`)
|
|
417
|
+
}
|
|
418
|
+
console.log(`Output format: ${ctx.values.output}`)
|
|
419
|
+
}
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
name: 'mycli',
|
|
423
|
+
version: '1.0.0',
|
|
424
|
+
plugins: [logger()]
|
|
425
|
+
}
|
|
426
|
+
)
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
## Combining Multiple Plugin Types
|
|
430
|
+
|
|
431
|
+
When working with multiple plugins, you often need to combine their extension types to create a comprehensive type definition for your commands. TypeScript's intersection operator (`&`) provides a clean way to merge multiple plugin extension types.
|
|
432
|
+
|
|
433
|
+
### Using TypeScript's Intersection Operator (&)
|
|
434
|
+
|
|
435
|
+
The `&` operator creates an intersection type that combines multiple type definitions. This is particularly useful when your command needs to access extensions from multiple plugins.
|
|
436
|
+
|
|
437
|
+
#### With Official Gunshi Plugins
|
|
438
|
+
|
|
439
|
+
When using official Gunshi plugins that provide plugin IDs, combine their extensions using Record types. Let's break this down step by step:
|
|
440
|
+
|
|
441
|
+
**Step 1: Import Plugin IDs and Types**
|
|
442
|
+
|
|
443
|
+
First, import the plugin IDs and extension types from the official plugins. Each plugin provides a unique identifier and type definition:
|
|
444
|
+
|
|
445
|
+
```ts
|
|
446
|
+
// Import plugin identifiers
|
|
447
|
+
import { pluginId as globalId } from '@gunshi/plugin-global'
|
|
448
|
+
import { pluginId as rendererId } from '@gunshi/plugin-renderer'
|
|
449
|
+
import { pluginId as i18nId } from '@gunshi/plugin-i18n'
|
|
450
|
+
|
|
451
|
+
// Import type definitions
|
|
452
|
+
import type { GlobalExtension, PluginId as GlobalId } from '@gunshi/plugin-global'
|
|
453
|
+
import type { UsageRendererExtension, PluginId as RendererId } from '@gunshi/plugin-renderer'
|
|
454
|
+
import type { I18nExtension, PluginId as I18nId } from '@gunshi/plugin-i18n'
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
**Step 2: Combine Extension Types**
|
|
458
|
+
|
|
459
|
+
Next, create a combined type that includes all the plugin extensions your command will use. The Record types map each plugin ID to its corresponding extension:
|
|
460
|
+
|
|
461
|
+
```ts
|
|
462
|
+
// Combine multiple plugin extension types using intersection (&)
|
|
463
|
+
type CombinedExtensions = Record<GlobalId, GlobalExtension> &
|
|
464
|
+
Record<RendererId, UsageRendererExtension> &
|
|
465
|
+
Record<I18nId, I18nExtension>
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
**Step 3: Define the Command with Combined Extensions**
|
|
469
|
+
|
|
470
|
+
Finally, use `defineWithTypes` to create a command that can access all the combined plugin extensions with full type safety:
|
|
471
|
+
|
|
472
|
+
```ts
|
|
473
|
+
// Use defineWithTypes with combined extensions
|
|
474
|
+
export default defineWithTypes<{ extensions: CombinedExtensions }>()({
|
|
475
|
+
name: 'greet',
|
|
476
|
+
args: {
|
|
477
|
+
name: { type: 'string', required: true }
|
|
478
|
+
},
|
|
479
|
+
run: async ctx => {
|
|
480
|
+
// Access i18n plugin extension
|
|
481
|
+
const locale = ctx.extensions[i18nId]?.locale
|
|
482
|
+
if (locale) {
|
|
483
|
+
console.log(`Current locale: ${locale.toString()}`)
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Access renderer plugin extension
|
|
487
|
+
const message = ctx.extensions[rendererId]?.text('welcome')
|
|
488
|
+
if (message) {
|
|
489
|
+
console.log(message)
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Access environment properties directly on context (not through extensions)
|
|
493
|
+
console.log(`Running in ${ctx.env.name || 'unknown'} environment`)
|
|
494
|
+
}
|
|
495
|
+
})
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
#### With Custom Plugins
|
|
499
|
+
|
|
500
|
+
Custom plugins provide plugin IDs and types. Import both to create fully type-safe command definitions:
|
|
501
|
+
|
|
502
|
+
```ts [commands/query.ts]
|
|
503
|
+
import { defineWithTypes } from 'gunshi'
|
|
504
|
+
import { pluginId as authId } from '../plugins/auth.ts'
|
|
505
|
+
import { pluginId as databaseId } from '../plugins/database.ts'
|
|
506
|
+
import { pluginId as loggerId } from '../plugins/logger.ts'
|
|
507
|
+
|
|
508
|
+
import type { AuthExtension, PluginId as AuthId } from '../plugins/auth.ts'
|
|
509
|
+
import type { DatabaseExtension, PluginId as DatabaseId } from '../plugins/database.ts'
|
|
510
|
+
import type { LoggerExtension, PluginId as LoggerId } from '../plugins/logger.ts'
|
|
511
|
+
|
|
512
|
+
type CombinedExtensions = Record<LoggerId, LoggerExtension> &
|
|
513
|
+
Record<AuthId, AuthExtension> &
|
|
514
|
+
Record<DatabaseId, DatabaseExtension>
|
|
515
|
+
|
|
516
|
+
export default defineWithTypes<{ extensions: CombinedExtensions }>()({
|
|
517
|
+
name: 'query',
|
|
518
|
+
description: 'Query database tables',
|
|
519
|
+
args: {
|
|
520
|
+
table: { type: 'string', required: true }
|
|
521
|
+
},
|
|
522
|
+
run: async ctx => {
|
|
523
|
+
// All extensions and arguments are fully typed
|
|
524
|
+
ctx.extensions[loggerId]?.log(`Querying ${ctx.values.table}`)
|
|
525
|
+
|
|
526
|
+
// Check read permissions for the specific table
|
|
527
|
+
if (!ctx.extensions[authId]?.hasPermission('read', ctx.values.table)) {
|
|
528
|
+
throw new Error(`No read access to table: ${ctx.values.table}`)
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// Perform the query using the database extension
|
|
532
|
+
const dataset = await ctx.extensions[databaseId]?.query(ctx.values.table)
|
|
533
|
+
console.log(`Retrieved records from ${ctx.values.table}`, dataset)
|
|
534
|
+
}
|
|
535
|
+
})
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
<!-- eslint-disable markdown/no-missing-label-refs -->
|
|
539
|
+
|
|
540
|
+
> [!TIP]
|
|
541
|
+
> The example fully code is [here](https://github.com/kazupon/gunshi/tree/main/playground/advanced/type-system/combine).
|
|
542
|
+
|
|
543
|
+
<!-- eslint-enable markdown/no-missing-label-refs -->
|
|
544
|
+
|
|
545
|
+
Both approaches achieve the same goal: combining multiple plugin extensions with full type safety. Use Record types when working with official plugins that provide plugin IDs, and direct intersection types for custom plugins.
|
|
546
|
+
|
|
547
|
+
## Choosing the Right Function
|
|
548
|
+
|
|
549
|
+
### Use `define` or `lazy` when:
|
|
550
|
+
|
|
551
|
+
- Your command doesn't use plugin extensions
|
|
552
|
+
- You only need type safety for command arguments
|
|
553
|
+
- This is the most common use case
|
|
554
|
+
|
|
555
|
+
### Use `defineWithTypes` or `lazyWithTypes` when:
|
|
556
|
+
|
|
557
|
+
- Your command uses plugin extensions
|
|
558
|
+
- You need to declare expected extension types
|
|
559
|
+
- You want IntelliSense for plugin APIs
|
|
560
|
+
|
|
561
|
+
The choice is straightforward: if you need plugin extensions, use the `WithTypes` variants; otherwise, use the standard functions for simpler, cleaner code.
|