@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.
@@ -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
+ ---