@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,529 @@
|
|
|
1
|
+
# Plugin Type System
|
|
2
|
+
|
|
3
|
+
Gunshi's plugin system leverages TypeScript's advanced type system to provide complete type safety.
|
|
4
|
+
|
|
5
|
+
This guide explains how to create type-safe plugins with proper type definitions.
|
|
6
|
+
|
|
7
|
+
## Introduction
|
|
8
|
+
|
|
9
|
+
Gunshi is designed with a **TypeScript-first** philosophy, providing:
|
|
10
|
+
|
|
11
|
+
- **Type inference** for plugin extensions and dependencies
|
|
12
|
+
- **Compile-time validation** of plugin interactions
|
|
13
|
+
- **IntelliSense support** throughout development
|
|
14
|
+
- **Type-safe plugin communication** between plugins
|
|
15
|
+
|
|
16
|
+
This guide focuses on TypeScript's type system for plugin development.
|
|
17
|
+
|
|
18
|
+
## Basic Type Definitions
|
|
19
|
+
|
|
20
|
+
Every type-safe plugin starts with two fundamental type definitions:
|
|
21
|
+
|
|
22
|
+
### Plugin ID and Extension Interface
|
|
23
|
+
|
|
24
|
+
The following code shows how to define and export a plugin's ID and extension interface in a separate types file:
|
|
25
|
+
|
|
26
|
+
```ts [types.ts]
|
|
27
|
+
// Define and export your plugin's types
|
|
28
|
+
export const pluginId = 'mycompany:logger' as const
|
|
29
|
+
export type PluginId = typeof pluginId
|
|
30
|
+
|
|
31
|
+
export interface LoggerExtension {
|
|
32
|
+
log: (message: string) => void
|
|
33
|
+
error: (message: string) => void
|
|
34
|
+
warn: (message: string) => void
|
|
35
|
+
debug: (message: string) => void
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**Key principles:**
|
|
40
|
+
|
|
41
|
+
- Literal types (`as const`) enable TypeScript to track specific plugin IDs
|
|
42
|
+
- Without `as const`, TypeScript widens the type to `string`, losing the specific ID value
|
|
43
|
+
- Literal types allow TypeScript to infer the exact key when accessing `ctx.extensions['mycompany:logger']`
|
|
44
|
+
- This enables autocomplete for available extensions and compile-time validation of plugin ID references
|
|
45
|
+
- Exported types allow other plugins and commands to reference your plugin
|
|
46
|
+
- Well-defined interfaces provide IntelliSense and compile-time validation
|
|
47
|
+
|
|
48
|
+
<!-- eslint-disable markdown/no-missing-label-refs -->
|
|
49
|
+
|
|
50
|
+
> [!TIP]
|
|
51
|
+
> Plugin consumers can use these exported interfaces to type their command context's extensions, enabling type-safe access to plugin functionality in their command runners. For detailed usage patterns of type-safe command definitions with plugin extensions, see [Advanced Type System](../advanced/type-system.md).
|
|
52
|
+
|
|
53
|
+
<!-- eslint-enable markdown/no-missing-label-refs -->
|
|
54
|
+
|
|
55
|
+
## The `plugin` Function Type Parameters
|
|
56
|
+
|
|
57
|
+
The `plugin` function uses TypeScript's generics to ensure complete type safety through four type parameters:
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
plugin<
|
|
61
|
+
DependencyExtensions, // Extensions from dependencies
|
|
62
|
+
PluginId, // Literal plugin ID
|
|
63
|
+
Dependencies, // Dependency array type
|
|
64
|
+
Extension // This plugin's extension type
|
|
65
|
+
>(options)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Each parameter serves a specific purpose:
|
|
69
|
+
|
|
70
|
+
- **DependencyExtensions**: Types of extensions this plugin depends on
|
|
71
|
+
- **PluginId**: The literal type of this plugin's ID
|
|
72
|
+
- **Dependencies**: The literal type of the dependencies array
|
|
73
|
+
- **Extension**: The type of extension this plugin provides
|
|
74
|
+
|
|
75
|
+
### Why These Type Parameters Are Necessary
|
|
76
|
+
|
|
77
|
+
While TypeScript can infer some types automatically, explicitly specifying all four type parameters provides several critical benefits:
|
|
78
|
+
|
|
79
|
+
1. **Complete Type Safety**: Ensures that dependency access in your extension is fully typed
|
|
80
|
+
2. **Compile-time Validation**: Catches plugin ID mismatches and missing dependencies before runtime
|
|
81
|
+
3. **Better IntelliSense**: Provides accurate autocompletion for `ctx.extensions` access
|
|
82
|
+
4. **Clear API Contracts**: Makes plugin dependencies and provided extensions explicit
|
|
83
|
+
|
|
84
|
+
### What Happens When Type Parameters Are Omitted
|
|
85
|
+
|
|
86
|
+
If you omit type parameters, TypeScript falls back to default or inferred types:
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
import { plugin } from 'gunshi/plugin'
|
|
90
|
+
|
|
91
|
+
// Without type parameters - loses type safety
|
|
92
|
+
const plugin1 = plugin({
|
|
93
|
+
id: 'my-plugin',
|
|
94
|
+
dependencies: ['other-plugin'],
|
|
95
|
+
extension: ctx => ({
|
|
96
|
+
method: () => {
|
|
97
|
+
// ctx.extensions['other-plugin'] is typed as 'any'
|
|
98
|
+
const other = ctx.extensions['other-plugin'] // No type checking!
|
|
99
|
+
return other.someMethod() // No IntelliSense, no error if method doesn't exist
|
|
100
|
+
}
|
|
101
|
+
})
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
// With type parameters - full type safety
|
|
105
|
+
const plugin2 = plugin<
|
|
106
|
+
{ 'other-plugin': OtherExtension },
|
|
107
|
+
'my-plugin',
|
|
108
|
+
['other-plugin'],
|
|
109
|
+
MyExtension
|
|
110
|
+
>({
|
|
111
|
+
id: 'my-plugin',
|
|
112
|
+
dependencies: ['other-plugin'],
|
|
113
|
+
extension: ctx => ({
|
|
114
|
+
method: () => {
|
|
115
|
+
// ctx.extensions['other-plugin'] is typed as OtherExtension
|
|
116
|
+
const other = ctx.extensions['other-plugin'] // Fully typed!
|
|
117
|
+
return other.someMethod() // IntelliSense works, compile error if method doesn't exist
|
|
118
|
+
}
|
|
119
|
+
})
|
|
120
|
+
})
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Without explicit type parameters:
|
|
124
|
+
|
|
125
|
+
- Dependencies are not type-checked against actual usage
|
|
126
|
+
- Extension access returns `any` type, losing all type safety
|
|
127
|
+
- Plugin IDs are treated as generic strings rather than literal types
|
|
128
|
+
- No compile-time validation of plugin interactions
|
|
129
|
+
|
|
130
|
+
## Progressive Type Safety Examples
|
|
131
|
+
|
|
132
|
+
Let's explore these type parameters through increasingly complex examples:
|
|
133
|
+
|
|
134
|
+
### 1. Simple Plugin (No Dependencies)
|
|
135
|
+
|
|
136
|
+
This example demonstrates a basic plugin without any dependencies, using only the essential type parameters:
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
import { plugin } from 'gunshi/plugin'
|
|
140
|
+
import { pluginId } from './types.ts'
|
|
141
|
+
|
|
142
|
+
import type { PluginId, LoggerExtension } from './types.ts'
|
|
143
|
+
|
|
144
|
+
export default function logger() {
|
|
145
|
+
return plugin<{}, PluginId, [], LoggerExtension>({
|
|
146
|
+
id: pluginId,
|
|
147
|
+
name: 'Logger Plugin',
|
|
148
|
+
|
|
149
|
+
extension: (): LoggerExtension => ({
|
|
150
|
+
log: msg => console.log(`[LOG] ${msg}`),
|
|
151
|
+
error: msg => console.error(`[ERROR] ${msg}`),
|
|
152
|
+
warn: msg => console.warn(`[WARN] ${msg}`),
|
|
153
|
+
debug: msg => console.debug(`[DEBUG] ${msg}`)
|
|
154
|
+
})
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### 2. Plugin with Dependencies
|
|
160
|
+
|
|
161
|
+
This example shows how to declare and use dependencies with proper type definitions:
|
|
162
|
+
|
|
163
|
+
```ts [api.ts]
|
|
164
|
+
import { plugin } from 'gunshi/plugin'
|
|
165
|
+
import { pluginId as loggerId } from '@mycompany/plugin-logger'
|
|
166
|
+
import { pluginId as authId } from '@mycompany/plugin-auth'
|
|
167
|
+
|
|
168
|
+
import type { LoggerExtension } from '@mycompany/plugin-logger'
|
|
169
|
+
import type { AuthExtension } from '@mycompany/plugin-auth'
|
|
170
|
+
|
|
171
|
+
export const pluginId = 'mycompany:api' as const
|
|
172
|
+
export type PluginId = typeof pluginId
|
|
173
|
+
|
|
174
|
+
export interface ApiExtension {
|
|
175
|
+
get: <T = unknown>(endpoint: string) => Promise<T>
|
|
176
|
+
post: <T = unknown>(endpoint: string, data: unknown) => Promise<T>
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Define dependency types using object notation
|
|
180
|
+
type DependencyExtensions = {
|
|
181
|
+
[loggerId]: LoggerExtension
|
|
182
|
+
[authId]: AuthExtension
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Define dependencies array
|
|
186
|
+
const dependencies = [loggerId, authId] as const
|
|
187
|
+
type Dependencies = typeof dependencies
|
|
188
|
+
|
|
189
|
+
export default function api() {
|
|
190
|
+
return plugin<DependencyExtensions, PluginId, Dependencies, ApiExtension>({
|
|
191
|
+
id: pluginId,
|
|
192
|
+
dependencies,
|
|
193
|
+
|
|
194
|
+
extension: ctx => {
|
|
195
|
+
const logger = ctx.extensions[loggerId] // Fully typed!
|
|
196
|
+
const auth = ctx.extensions[authId] // Fully typed!
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
get: async endpoint => {
|
|
200
|
+
logger.log(`GET ${endpoint}`)
|
|
201
|
+
const token = auth.getToken()
|
|
202
|
+
// Implementation...
|
|
203
|
+
},
|
|
204
|
+
post: async (endpoint, data) => {
|
|
205
|
+
logger.log(`POST ${endpoint}`)
|
|
206
|
+
// Implementation...
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
})
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### 3. Plugin with Optional Dependencies
|
|
215
|
+
|
|
216
|
+
Gunshi supports both required and optional plugin dependencies with full type safety.
|
|
217
|
+
|
|
218
|
+
The following example shows how to define both required and optional dependencies with their corresponding TypeScript types:
|
|
219
|
+
|
|
220
|
+
```ts [metrics.ts]
|
|
221
|
+
import { plugin } from 'gunshi/plugin'
|
|
222
|
+
import { pluginId as loggerId } from './logger.ts'
|
|
223
|
+
import { pluginId as cacheId } from './cache.ts'
|
|
224
|
+
|
|
225
|
+
import type { LoggerExtension } from './logger.ts'
|
|
226
|
+
import type { CacheExtension } from './cache.ts'
|
|
227
|
+
|
|
228
|
+
// Type definition: cache is optional
|
|
229
|
+
type DependencyExtensions = {
|
|
230
|
+
[loggerId]: LoggerExtension
|
|
231
|
+
[cacheId]?: CacheExtension // Optional with ?
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Runtime declaration: must match types
|
|
235
|
+
const dependencies = [
|
|
236
|
+
loggerId, // Required
|
|
237
|
+
{ id: cacheId, optional: true } // Optional
|
|
238
|
+
] as const
|
|
239
|
+
|
|
240
|
+
export const pluginId = 'mycompany:metrics' as const
|
|
241
|
+
export type PluginId = typeof pluginId
|
|
242
|
+
|
|
243
|
+
export interface MetricsExtension {
|
|
244
|
+
// ...
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export default function metrics() {
|
|
248
|
+
return plugin<DependencyExtensions, typeof pluginId, typeof dependencies, MetricsExtension>({
|
|
249
|
+
id: pluginId,
|
|
250
|
+
dependencies,
|
|
251
|
+
|
|
252
|
+
extension: ctx => {
|
|
253
|
+
const logger = ctx.extensions[loggerId] // Always defined
|
|
254
|
+
const cache = ctx.extensions[cacheId] // Possibly undefined
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
track: (event: string) => {
|
|
258
|
+
logger.log(`Event: ${event}`)
|
|
259
|
+
|
|
260
|
+
// Safe optional access
|
|
261
|
+
if (cache) {
|
|
262
|
+
cache.set(`event:${event}`, Date.now())
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
})
|
|
268
|
+
}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### 4. Dependency Chain
|
|
272
|
+
|
|
273
|
+
Plugins can depend on other plugins that have their own dependencies.
|
|
274
|
+
|
|
275
|
+
This example demonstrates a three-level dependency chain where each plugin builds on the previous ones:
|
|
276
|
+
|
|
277
|
+
```ts [base.ts]
|
|
278
|
+
// No dependencies
|
|
279
|
+
export const baseId = 'base' as const
|
|
280
|
+
export interface BaseExtension {
|
|
281
|
+
getConfig: () => Config
|
|
282
|
+
}
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
```ts [logger.ts]
|
|
286
|
+
import { plugin } from 'gunshi/plugin'
|
|
287
|
+
import { baseId } from './base.ts'
|
|
288
|
+
|
|
289
|
+
import type { BaseExtension } from './base.ts'
|
|
290
|
+
|
|
291
|
+
// Depends on base
|
|
292
|
+
export const loggerId = 'logger' as const
|
|
293
|
+
export interface LoggerExtension {
|
|
294
|
+
log: (msg: string) => void
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const loggerDeps = [baseId] as const
|
|
298
|
+
|
|
299
|
+
export default plugin<
|
|
300
|
+
{ [baseId]: BaseExtension },
|
|
301
|
+
typeof loggerId,
|
|
302
|
+
typeof loggerDeps,
|
|
303
|
+
LoggerExtension
|
|
304
|
+
>({
|
|
305
|
+
id: loggerId,
|
|
306
|
+
dependencies: loggerDeps,
|
|
307
|
+
extension: ctx => {
|
|
308
|
+
const config = ctx.extensions[baseId].getConfig()
|
|
309
|
+
return {
|
|
310
|
+
log: msg => {
|
|
311
|
+
if (config.verbose) console.log(msg)
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
})
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
```ts [api.ts]
|
|
319
|
+
import { plugin } from 'gunshi/plugin'
|
|
320
|
+
import { baseId } from './base.ts'
|
|
321
|
+
import { loggerId } from './logger.ts'
|
|
322
|
+
|
|
323
|
+
import type { BaseExtension } from './base.ts'
|
|
324
|
+
import type { LoggerExtension } from './logger.ts'
|
|
325
|
+
|
|
326
|
+
export const apiId = 'api' as const
|
|
327
|
+
|
|
328
|
+
export interface ApiExtension {
|
|
329
|
+
request: (url: string) => Promise<void> | void
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Depends on both
|
|
333
|
+
const apiDeps = [baseId, loggerId] as const
|
|
334
|
+
|
|
335
|
+
export default plugin<
|
|
336
|
+
{
|
|
337
|
+
[baseId]: BaseExtension
|
|
338
|
+
[loggerId]: LoggerExtension
|
|
339
|
+
},
|
|
340
|
+
typeof apiId,
|
|
341
|
+
typeof apiDeps,
|
|
342
|
+
ApiExtension
|
|
343
|
+
>({
|
|
344
|
+
id: apiId,
|
|
345
|
+
dependencies: apiDeps,
|
|
346
|
+
extension: ctx => {
|
|
347
|
+
const logger = ctx.extensions[loggerId]
|
|
348
|
+
const config = ctx.extensions[baseId].getConfig()
|
|
349
|
+
|
|
350
|
+
return {
|
|
351
|
+
request: async (url: string) => {
|
|
352
|
+
logger.log(`API Request: ${url}`)
|
|
353
|
+
// Implementation...
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
})
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
## Complete Example
|
|
361
|
+
|
|
362
|
+
This example demonstrates all concepts together: type definitions, all four type parameters, and dependency management.
|
|
363
|
+
|
|
364
|
+
The following code shows a production-ready API plugin with proper type exports, dependency handling, and complete implementation:
|
|
365
|
+
|
|
366
|
+
```ts [types.ts]
|
|
367
|
+
// Type definitions for the API plugin
|
|
368
|
+
export const pluginId = 'mycompany:api' as const
|
|
369
|
+
export type PluginId = typeof pluginId
|
|
370
|
+
|
|
371
|
+
export interface ApiExtension {
|
|
372
|
+
get: <T = unknown>(endpoint: string) => Promise<T>
|
|
373
|
+
post: <T = unknown>(endpoint: string, data: unknown) => Promise<T>
|
|
374
|
+
delete: (endpoint: string) => Promise<void>
|
|
375
|
+
}
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
```ts [api.ts]
|
|
379
|
+
import { plugin } from 'gunshi/plugin'
|
|
380
|
+
import { pluginId } from './types.ts'
|
|
381
|
+
import { pluginId as loggerId } from './logger.ts'
|
|
382
|
+
import { pluginId as authId } from './auth.ts'
|
|
383
|
+
|
|
384
|
+
import type { PluginId, ApiExtension } from './types.ts'
|
|
385
|
+
import type { LoggerExtension } from './logger.ts'
|
|
386
|
+
import type { AuthExtension } from './auth.ts'
|
|
387
|
+
|
|
388
|
+
// Re-export for consumers
|
|
389
|
+
export * from './types.ts'
|
|
390
|
+
|
|
391
|
+
// Define dependency types
|
|
392
|
+
type DependencyExtensions = {
|
|
393
|
+
[loggerId]: LoggerExtension // Required
|
|
394
|
+
[authId]: AuthExtension // Required
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Define dependencies array
|
|
398
|
+
const dependencies = [loggerId, authId] as const
|
|
399
|
+
type Dependencies = typeof dependencies
|
|
400
|
+
|
|
401
|
+
// Export the plugin factory
|
|
402
|
+
export default function api(baseUrl: string) {
|
|
403
|
+
return plugin<DependencyExtensions, PluginId, Dependencies, ApiExtension>({
|
|
404
|
+
id: pluginId,
|
|
405
|
+
name: 'API Plugin',
|
|
406
|
+
dependencies,
|
|
407
|
+
|
|
408
|
+
extension: ctx => {
|
|
409
|
+
const logger = ctx.extensions[loggerId]
|
|
410
|
+
const auth = ctx.extensions[authId]
|
|
411
|
+
|
|
412
|
+
async function request<T = unknown>(
|
|
413
|
+
method: string,
|
|
414
|
+
endpoint: string,
|
|
415
|
+
data?: Record<string, unknown>
|
|
416
|
+
) {
|
|
417
|
+
const url = `${baseUrl}${endpoint}`
|
|
418
|
+
|
|
419
|
+
// Make request
|
|
420
|
+
logger.log(`${method} ${url}`)
|
|
421
|
+
const token = auth.getToken()
|
|
422
|
+
|
|
423
|
+
// Simulate API call (replace with actual fetch in production)
|
|
424
|
+
const result = await simulateApiCall(method, endpoint, data || {}, token)
|
|
425
|
+
|
|
426
|
+
return result as T
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
return {
|
|
430
|
+
get: endpoint => request('GET', endpoint),
|
|
431
|
+
post: (endpoint, data) => request('POST', endpoint, data),
|
|
432
|
+
delete: async endpoint => {
|
|
433
|
+
await request('DELETE', endpoint)
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
})
|
|
438
|
+
}
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
Usage in your CLI application:
|
|
442
|
+
|
|
443
|
+
```ts [cli.ts]
|
|
444
|
+
import { cli, define } from 'gunshi'
|
|
445
|
+
import api, { pluginId as apiId } from './api.ts'
|
|
446
|
+
import auth from './auth.ts'
|
|
447
|
+
import logger from './logger.ts'
|
|
448
|
+
|
|
449
|
+
import type { Args, GunshiParams } from 'gunshi'
|
|
450
|
+
import type { ApiExtension } from './api.ts'
|
|
451
|
+
|
|
452
|
+
const fetchArgs = {
|
|
453
|
+
endpoint: {
|
|
454
|
+
type: 'string',
|
|
455
|
+
required: true,
|
|
456
|
+
description: 'API endpoint to fetch'
|
|
457
|
+
}
|
|
458
|
+
} as const satisfies Args
|
|
459
|
+
|
|
460
|
+
// Define a command that uses the API plugin
|
|
461
|
+
const fetchCommand = define<
|
|
462
|
+
GunshiParams<{
|
|
463
|
+
args: typeof fetchArgs
|
|
464
|
+
extensions: { [apiId]: ApiExtension }
|
|
465
|
+
}>
|
|
466
|
+
>({
|
|
467
|
+
name: 'fetch',
|
|
468
|
+
description: 'Fetch data from API',
|
|
469
|
+
args: fetchArgs,
|
|
470
|
+
run: async ctx => {
|
|
471
|
+
const api = ctx.extensions[apiId]
|
|
472
|
+
const data = await api.get(ctx.values.endpoint)
|
|
473
|
+
console.log(JSON.stringify(data, null, 2))
|
|
474
|
+
}
|
|
475
|
+
})
|
|
476
|
+
|
|
477
|
+
// Configure and run CLI
|
|
478
|
+
await cli(process.argv.slice(2), fetchCommand, {
|
|
479
|
+
name: 'my-cli',
|
|
480
|
+
version: '1.0.0',
|
|
481
|
+
plugins: [
|
|
482
|
+
// Dependencies must be registered first
|
|
483
|
+
logger(),
|
|
484
|
+
auth({ token: process.env.API_TOKEN }),
|
|
485
|
+
api('https://api.example.com')
|
|
486
|
+
]
|
|
487
|
+
})
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
<!-- eslint-disable markdown/no-missing-label-refs -->
|
|
491
|
+
|
|
492
|
+
> [!TIP]
|
|
493
|
+
> The example fully code is [here](https://github.com/kazupon/gunshi/tree/main/playground/plugins/type-system).
|
|
494
|
+
|
|
495
|
+
<!-- eslint-enable markdown/no-missing-label-refs -->
|
|
496
|
+
|
|
497
|
+
When executed, the plugins work together seamlessly:
|
|
498
|
+
|
|
499
|
+
```sh
|
|
500
|
+
API_TOKEN=xxx npx tsx cli.ts fetch --endpoint /users
|
|
501
|
+
my-cli (my-cli v1.0.0)
|
|
502
|
+
|
|
503
|
+
[LOG] GET https://api.example.com/users
|
|
504
|
+
[
|
|
505
|
+
{
|
|
506
|
+
"id": 1,
|
|
507
|
+
"name": "Alice"
|
|
508
|
+
},
|
|
509
|
+
{
|
|
510
|
+
"id": 2,
|
|
511
|
+
"name": "Bob"
|
|
512
|
+
}
|
|
513
|
+
]
|
|
514
|
+
|
|
515
|
+
API_TOKEN=xxx npx tsx cli.ts fetch --endpoint /users/1
|
|
516
|
+
my-cli (my-cli v1.0.0)
|
|
517
|
+
|
|
518
|
+
[LOG] GET https://api.example.com/users/1
|
|
519
|
+
{
|
|
520
|
+
"id": 1,
|
|
521
|
+
"name": "Alice"
|
|
522
|
+
}
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
## Next Steps
|
|
526
|
+
|
|
527
|
+
With a strong foundation in type-safe plugin development, you've learned how to create plugins that provide compile-time guarantees and excellent developer experience through TypeScript's type system.
|
|
528
|
+
|
|
529
|
+
Before sharing your plugins with others, it's crucial to ensure they work correctly. The next chapter, [Plugin Testing](./testing.md), will guide you through comprehensive testing strategies for plugins, including unit tests, integration tests, and testing plugin interactions.
|
package/src/index.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
# https://vitepress.dev/reference/default-theme-home-page
|
|
3
|
+
layout: home
|
|
4
|
+
|
|
5
|
+
hero:
|
|
6
|
+
name: 'Gunshi'
|
|
7
|
+
text: 'Modern JavaScript Command-line Library'
|
|
8
|
+
tagline: 'Robust, modular, flexible, and localizable CLI library'
|
|
9
|
+
image:
|
|
10
|
+
src: /logo.png
|
|
11
|
+
alt: Gunshi
|
|
12
|
+
actions:
|
|
13
|
+
- theme: brand
|
|
14
|
+
text: Get Started
|
|
15
|
+
link: /guide/introduction/what-is-gunshi
|
|
16
|
+
- theme: alt
|
|
17
|
+
text: View on GitHub
|
|
18
|
+
link: https://github.com/kazupon/gunshi
|
|
19
|
+
|
|
20
|
+
features:
|
|
21
|
+
- icon: 📏
|
|
22
|
+
title: Simple & Universal
|
|
23
|
+
details: Run commands with simple API and support for universal runtime (Node.js, Deno, Bun).
|
|
24
|
+
|
|
25
|
+
- icon: ⚙️
|
|
26
|
+
title: Declarative & Type Safe
|
|
27
|
+
details: Configure commands declaratively with full TypeScript support and type-safe argument parsing.
|
|
28
|
+
|
|
29
|
+
- icon: 🧩
|
|
30
|
+
title: Composable & Lazy
|
|
31
|
+
details: Create modular sub-commands with context sharing and lazy loading for better performance.
|
|
32
|
+
|
|
33
|
+
- icon: 🎨
|
|
34
|
+
title: Flexible Rendering
|
|
35
|
+
details: Customize usage generation, validation errors, and help messages with pluggable renderers.
|
|
36
|
+
|
|
37
|
+
- icon: 🌍
|
|
38
|
+
title: Internationalization
|
|
39
|
+
details: Built with global users in mind, featuring locale-aware design, resource management, and multi-language support.
|
|
40
|
+
|
|
41
|
+
- icon: 🔌
|
|
42
|
+
title: Pluggable
|
|
43
|
+
details: Extensible plugin system with dependency management and lifecycle hooks for modular CLI development.
|
|
44
|
+
---
|