@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,722 @@
|
|
|
1
|
+
# Gunshi v0.27 Release Notes
|
|
2
|
+
|
|
3
|
+
## ๐ Features
|
|
4
|
+
|
|
5
|
+
### Plugin System
|
|
6
|
+
|
|
7
|
+
Gunshi v0.27 introduces a powerful plugin system that enables modular CLI architecture. Plugins can extend command functionality, add global options, and provide cross-cutting concerns like internationalization and completion.
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { cli } from 'gunshi'
|
|
11
|
+
import i18n, { defineI18nWithTypes, pluginId as i18nId } from '@gunshi/plugin-i18n'
|
|
12
|
+
import completion from '@gunshi/plugin-completion'
|
|
13
|
+
import type { I18nExtension, PluginId as I18nPluginId } from '@gunshi/plugin-i18n'
|
|
14
|
+
|
|
15
|
+
// Type-safe command definition with plugin extensions
|
|
16
|
+
const command = defineI18nWithTypes<{ extensions: Record<I18nPluginId, I18nExtension> }>()({
|
|
17
|
+
name: 'app',
|
|
18
|
+
// Provide translation resources for i18n plugin
|
|
19
|
+
resource: locale => {
|
|
20
|
+
if (locale.toString() === 'ja-JP') {
|
|
21
|
+
return { greeting: 'ใใใซใกใฏ' }
|
|
22
|
+
}
|
|
23
|
+
return { greeting: 'Hello' }
|
|
24
|
+
},
|
|
25
|
+
run: ctx => {
|
|
26
|
+
// Plugins automatically extend the context
|
|
27
|
+
console.log(ctx.extensions[i18nId].translate('greeting'))
|
|
28
|
+
}
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
await cli(process.argv.slice(2), command, {
|
|
32
|
+
name: 'my-app',
|
|
33
|
+
version: '1.0.0',
|
|
34
|
+
plugins: [i18n(), completion()]
|
|
35
|
+
})
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Plugins can extend the `CommandContext` by adding their own properties to `ctx.extensions`. Each plugin's extension is namespaced using its plugin ID, preventing conflicts and ensuring type safety:
|
|
39
|
+
|
|
40
|
+
- Extensions are accessed via `ctx.extensions[pluginId]`
|
|
41
|
+
- Plugin IDs should be imported to avoid hardcoding strings
|
|
42
|
+
- TypeScript provides full type inference for extensions when using `define<Record<PluginId, Extension>>()`
|
|
43
|
+
- Multiple plugins can coexist, each providing their own extensions
|
|
44
|
+
|
|
45
|
+
In the example above, the i18n plugin extends `ctx.extensions` with internationalization functionality accessible via `ctx.extensions[i18nId]`. This provides methods like `translate()` and properties like `locale` to all commands, enabling consistent internationalization across your CLI application.
|
|
46
|
+
|
|
47
|
+
### Official Plugin Ecosystem
|
|
48
|
+
|
|
49
|
+
Gunshi v0.27 comes with a comprehensive set of official plugins that work seamlessly with the new plugin system:
|
|
50
|
+
|
|
51
|
+
#### Built-in Plugins (included in `gunshi` package)
|
|
52
|
+
|
|
53
|
+
These plugins are automatically included when using the standard `cli()` function from gunshi:
|
|
54
|
+
|
|
55
|
+
- `@gunshi/plugin-global`: Adds `--help` and `--version` options to all commands automatically
|
|
56
|
+
- Provides `showVersion()`, `showHeader()`, `showUsage()`, and `showValidationErrors()` methods
|
|
57
|
+
- Intercepts command execution to handle help/version display
|
|
58
|
+
|
|
59
|
+
- `@gunshi/plugin-renderer`: Automatic rendering of usage, help text, and validation errors
|
|
60
|
+
- Integrates with i18n plugin for localized messages
|
|
61
|
+
- Customizable rendering for headers, usage, and error messages
|
|
62
|
+
- Smart formatting for arguments, options, and subcommands
|
|
63
|
+
|
|
64
|
+
#### Optional Plugins
|
|
65
|
+
|
|
66
|
+
Install these plugins separately based on your needs:
|
|
67
|
+
|
|
68
|
+
- `gunshi/plugin-i18n`: Comprehensive internationalization support
|
|
69
|
+
|
|
70
|
+
```sh
|
|
71
|
+
npm install @gunshi/plugin-i18n
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
- Type-safe translation functions with key completion
|
|
75
|
+
- Dynamic resource loading per command
|
|
76
|
+
- Custom translation adapters
|
|
77
|
+
- Built-in namespace management
|
|
78
|
+
|
|
79
|
+
- `@gunshi/plugin-completion`: Shell completion support
|
|
80
|
+
|
|
81
|
+
```sh
|
|
82
|
+
npm install @gunshi/plugin-completion
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
- Support for bash, zsh, fish, and PowerShell
|
|
86
|
+
- Custom completion handlers for dynamic suggestions
|
|
87
|
+
- Automatic `complete` subcommand generation
|
|
88
|
+
- Integration with i18n for localized descriptions
|
|
89
|
+
|
|
90
|
+
### `@gunshi/definition` - Command Definition Utilities
|
|
91
|
+
|
|
92
|
+
New standalone package for creating reusable, distributable commands:
|
|
93
|
+
|
|
94
|
+
```sh
|
|
95
|
+
npm install @gunshi/definition
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
This package enables you to create standalone commands that can be published as libraries and shared across multiple CLI applications:
|
|
99
|
+
|
|
100
|
+
```js
|
|
101
|
+
// my-shared-commands/build.js
|
|
102
|
+
import { define } from '@gunshi/definition'
|
|
103
|
+
|
|
104
|
+
// Create a standalone command that can be imported by any Gunshi CLI
|
|
105
|
+
export const buildCommand = define({
|
|
106
|
+
name: 'build',
|
|
107
|
+
args: {
|
|
108
|
+
watch: { type: 'boolean', default: false },
|
|
109
|
+
output: { type: 'string', required: true }
|
|
110
|
+
},
|
|
111
|
+
run: ctx => {
|
|
112
|
+
if (ctx.values.watch) {
|
|
113
|
+
console.log(`Watching and building to ${ctx.values.output}`)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
// my-shared-commands/analyze.js
|
|
119
|
+
import { lazy } from '@gunshi/definition'
|
|
120
|
+
|
|
121
|
+
// Export a lazy-loaded command for better performance
|
|
122
|
+
export const analyzeCommand = lazy(async () => {
|
|
123
|
+
// Heavy imports only when command is actually used
|
|
124
|
+
const { analyze } = await import('./heavy-analyzer.js')
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
name: 'analyze',
|
|
128
|
+
run: ctx => analyze(ctx.values)
|
|
129
|
+
}
|
|
130
|
+
})
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Now these commands can be imported and used in any Gunshi CLI:
|
|
134
|
+
|
|
135
|
+
```js
|
|
136
|
+
// app-cli/index.js
|
|
137
|
+
import { cli } from 'gunshi'
|
|
138
|
+
import { buildCommand, analyzeCommand } from 'my-shared-commands'
|
|
139
|
+
|
|
140
|
+
await cli(process.argv.slice(2), buildCommand, {
|
|
141
|
+
name: 'app',
|
|
142
|
+
subCommands: {
|
|
143
|
+
build: buildCommand,
|
|
144
|
+
analyze: analyzeCommand
|
|
145
|
+
}
|
|
146
|
+
})
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Benefits:
|
|
150
|
+
|
|
151
|
+
- **Standalone commands**: Create reusable commands that can be published as npm packages
|
|
152
|
+
- **Framework-agnostic distribution**: Commands defined with `@gunshi/definition` can be imported by any Gunshi CLI
|
|
153
|
+
- **Smaller bundles**: Separate from main gunshi package for optimal tree-shaking
|
|
154
|
+
- **Full type safety**: Complete TypeScript inference for arguments and extensions (see [Type Safety Enhancements](#type-safety-enhancements) for details)
|
|
155
|
+
- **Mix and match**: Combine commands from multiple packages to build your CLI
|
|
156
|
+
|
|
157
|
+
### `@gunshi/bone` - Minimal CLI Package
|
|
158
|
+
|
|
159
|
+
A lightweight alternative to the main gunshi package, without any built-in plugins:
|
|
160
|
+
|
|
161
|
+
```sh
|
|
162
|
+
npm install @gunshi/bone
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
```js
|
|
166
|
+
import { cli } from '@gunshi/bone'
|
|
167
|
+
import customPlugin from './my-custom-plugin.js'
|
|
168
|
+
|
|
169
|
+
// No default plugins - you have full control
|
|
170
|
+
await cli(process.argv.slice(2), command, {
|
|
171
|
+
name: 'my-minimal-cli',
|
|
172
|
+
plugins: [customPlugin()] // Only the plugins you need
|
|
173
|
+
})
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Use cases:
|
|
177
|
+
|
|
178
|
+
- When you need complete control over plugin composition
|
|
179
|
+
- For minimal bundle size in size-critical applications
|
|
180
|
+
- When built-in plugins conflict with your custom implementation
|
|
181
|
+
- For embedded CLIs where every byte counts
|
|
182
|
+
|
|
183
|
+
### Fallback to Entry Command
|
|
184
|
+
|
|
185
|
+
New `fallbackToEntry` option enables graceful handling of unknown subcommands:
|
|
186
|
+
|
|
187
|
+
```js
|
|
188
|
+
const mainCommand = {
|
|
189
|
+
name: 'tool',
|
|
190
|
+
args: {
|
|
191
|
+
file: { type: 'string', positional: true }
|
|
192
|
+
},
|
|
193
|
+
run: ctx => {
|
|
194
|
+
// Process file when no subcommand is provided
|
|
195
|
+
console.log(`Processing ${ctx.values.file}`)
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
await cli(process.argv.slice(2), mainCommand, {
|
|
200
|
+
name: 'tool',
|
|
201
|
+
fallbackToEntry: true, // Enable fallback
|
|
202
|
+
subCommands: {
|
|
203
|
+
convert: convertCommand,
|
|
204
|
+
validate: validateCommand
|
|
205
|
+
}
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
// Usage:
|
|
209
|
+
// tool convert file.txt -> Runs convert subcommand
|
|
210
|
+
// tool file.txt -> Falls back to main command
|
|
211
|
+
// tool unknown file.txt -> Falls back to main command (with v0.27)
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
This feature is particularly useful for:
|
|
215
|
+
|
|
216
|
+
- Git-style CLIs where the main command has its own functionality
|
|
217
|
+
- Backward compatibility when adding subcommands to existing CLIs
|
|
218
|
+
- Creating more forgiving user experiences
|
|
219
|
+
|
|
220
|
+
### Subcommand Object Style
|
|
221
|
+
|
|
222
|
+
Subcommands can now be defined as plain objects (Record) in addition to Map, providing better organization for complex CLIs:
|
|
223
|
+
|
|
224
|
+
```js
|
|
225
|
+
// Define subcommands
|
|
226
|
+
const addCommand = {
|
|
227
|
+
name: 'add',
|
|
228
|
+
description: 'Add file contents to the index',
|
|
229
|
+
run: ctx => console.log('Adding files...')
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const commitCommand = {
|
|
233
|
+
name: 'commit',
|
|
234
|
+
description: 'Record changes to the repository',
|
|
235
|
+
run: ctx => console.log('Committing...')
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Pass subcommands as object to CLI options
|
|
239
|
+
await cli(process.argv.slice(2), mainCommand, {
|
|
240
|
+
name: 'git',
|
|
241
|
+
version: '1.0.0',
|
|
242
|
+
subCommands: {
|
|
243
|
+
add: addCommand,
|
|
244
|
+
commit: commitCommand
|
|
245
|
+
}
|
|
246
|
+
})
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### Explicit Argument Detection
|
|
250
|
+
|
|
251
|
+
New feature to detect whether arguments were explicitly provided by the user or using default values:
|
|
252
|
+
|
|
253
|
+
```js
|
|
254
|
+
const command = {
|
|
255
|
+
name: 'deploy',
|
|
256
|
+
args: {
|
|
257
|
+
verbose: {
|
|
258
|
+
type: 'boolean',
|
|
259
|
+
default: false,
|
|
260
|
+
description: 'Enable verbose output'
|
|
261
|
+
},
|
|
262
|
+
output: {
|
|
263
|
+
type: 'string',
|
|
264
|
+
default: 'output.txt',
|
|
265
|
+
description: 'Output file path'
|
|
266
|
+
}
|
|
267
|
+
},
|
|
268
|
+
run: ctx => {
|
|
269
|
+
// Check if arguments were explicitly provided
|
|
270
|
+
if (ctx.explicit.output) {
|
|
271
|
+
console.log('Output file explicitly specified:', ctx.values.output)
|
|
272
|
+
} else {
|
|
273
|
+
console.log('Using default output file:', ctx.values.output)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Useful for configuration overrides
|
|
277
|
+
if (!ctx.explicit.verbose && ctx.values.verbose) {
|
|
278
|
+
console.log('Verbose mode enabled by config file')
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
The `ctx.explicit` object indicates for each argument:
|
|
285
|
+
|
|
286
|
+
- `true`: The argument was explicitly provided via command line
|
|
287
|
+
- `false`: The argument uses a default value or is undefined
|
|
288
|
+
|
|
289
|
+
### Command Lifecycle Hooks
|
|
290
|
+
|
|
291
|
+
New hooks for fine-grained control over command execution, available in CLI options:
|
|
292
|
+
|
|
293
|
+
```js
|
|
294
|
+
const command = {
|
|
295
|
+
name: 'backup',
|
|
296
|
+
run: ctx => {
|
|
297
|
+
// Main backup logic
|
|
298
|
+
console.log('Running backup...')
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
await cli(process.argv.slice(2), command, {
|
|
303
|
+
name: 'backup-tool',
|
|
304
|
+
version: '1.0.0',
|
|
305
|
+
onBeforeCommand: async ctx => {
|
|
306
|
+
console.log('Preparing backup...')
|
|
307
|
+
},
|
|
308
|
+
onAfterCommand: async (ctx, result) => {
|
|
309
|
+
console.log('Backup completed!')
|
|
310
|
+
if (result) console.log('Result:', result)
|
|
311
|
+
},
|
|
312
|
+
onErrorCommand: async (ctx, error) => {
|
|
313
|
+
console.error('Backup failed:', error)
|
|
314
|
+
// Custom error handling logic
|
|
315
|
+
}
|
|
316
|
+
})
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
### Custom Rendering Control
|
|
320
|
+
|
|
321
|
+
New rendering options allow fine-grained control over how commands display their output. Each command can customize or disable specific parts of the UI:
|
|
322
|
+
|
|
323
|
+
```js
|
|
324
|
+
const command = {
|
|
325
|
+
name: 'wizard',
|
|
326
|
+
rendering: {
|
|
327
|
+
// Custom header with emoji and version
|
|
328
|
+
header: async ctx => `๐ ${ctx.name} v${ctx.version}`,
|
|
329
|
+
|
|
330
|
+
// Disable default usage (set to null)
|
|
331
|
+
usage: null,
|
|
332
|
+
|
|
333
|
+
// Custom error formatting
|
|
334
|
+
validationErrors: async (ctx, error) => `โ Error: ${error.message}`
|
|
335
|
+
},
|
|
336
|
+
run: ctx => {
|
|
337
|
+
// Command logic
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
This feature enables:
|
|
343
|
+
|
|
344
|
+
- **Per-command customization**: Each command can have its own UI style
|
|
345
|
+
- **Selective rendering**: Disable specific parts (header, usage, errors) by setting them to `null`
|
|
346
|
+
- **Async rendering**: Support for async functions to fetch dynamic content
|
|
347
|
+
- **Full context access**: Renderers receive the complete command context for flexible output generation
|
|
348
|
+
|
|
349
|
+
## โก Improvement Features
|
|
350
|
+
|
|
351
|
+
### Type Safety Enhancements
|
|
352
|
+
|
|
353
|
+
v0.27 brings comprehensive type safety improvements to all core APIs through generic type parameters, enabling full TypeScript inference for arguments and plugin extensions.
|
|
354
|
+
|
|
355
|
+
#### Enhanced `define()` API
|
|
356
|
+
|
|
357
|
+
The `define` function now accepts type parameters for type-safe command definitions with plugin extensions:
|
|
358
|
+
|
|
359
|
+
```ts
|
|
360
|
+
import { define } from 'gunshi'
|
|
361
|
+
import logger, { pluginId as loggerId } from '@your-company/plugin-logger'
|
|
362
|
+
import auth, { pluginId as authId } from '@your-company/plugin-auth'
|
|
363
|
+
|
|
364
|
+
import type { LoggerExtension, PluginId as LoggerId } from '@your-company/plugin-logger'
|
|
365
|
+
import type { AuthExtension, PluginId as AuthId } from '@your-company/plugin-auth'
|
|
366
|
+
|
|
367
|
+
// Type-safe command with plugin extensions
|
|
368
|
+
const deployCommand = define<{
|
|
369
|
+
extensions: Record<LoggerId, LoggerExtension> & Record<AuthId, AuthExtension>
|
|
370
|
+
}>({
|
|
371
|
+
name: 'deploy',
|
|
372
|
+
run: ctx => {
|
|
373
|
+
// Plugin extensions are fully typed
|
|
374
|
+
ctx.extensions[loggerId]?.log('Starting deployment...')
|
|
375
|
+
|
|
376
|
+
if (!ctx.extensions[authId]?.isAuthenticated()) {
|
|
377
|
+
throw new Error('Authentication required')
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const user = ctx.extensions[authId]?.getUser()
|
|
381
|
+
ctx.extensions[loggerId]?.info(`Deploying as ${user?.name}`)
|
|
382
|
+
}
|
|
383
|
+
})
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
#### New `defineWithTypes()` API
|
|
387
|
+
|
|
388
|
+
For better type inference with plugin extensions, v0.27 introduces `defineWithTypes()` - a curried function that separates type specification from command definition:
|
|
389
|
+
|
|
390
|
+
```ts
|
|
391
|
+
import { defineWithTypes } from 'gunshi'
|
|
392
|
+
|
|
393
|
+
// Define your extensions type first
|
|
394
|
+
type MyExtensions = {
|
|
395
|
+
logger: { log: (msg: string) => void }
|
|
396
|
+
auth: { isAuthenticated: () => boolean; getUser: () => User }
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Use defineWithTypes - specify only extensions, args are inferred!
|
|
400
|
+
const command = defineWithTypes<{ extensions: MyExtensions }>()({
|
|
401
|
+
name: 'deploy',
|
|
402
|
+
args: {
|
|
403
|
+
env: { type: 'string', required: true },
|
|
404
|
+
force: { type: 'boolean', default: false }
|
|
405
|
+
},
|
|
406
|
+
run: ctx => {
|
|
407
|
+
// ctx.values is automatically inferred as { env: string; force?: boolean }
|
|
408
|
+
// ctx.extensions is typed as MyExtensions
|
|
409
|
+
if (!ctx.extensions.auth?.isAuthenticated()) {
|
|
410
|
+
throw new Error('Authentication required')
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
ctx.extensions.logger?.log(`Deploying to ${ctx.values.env}`)
|
|
414
|
+
}
|
|
415
|
+
})
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
Benefits over standard `define()`:
|
|
419
|
+
|
|
420
|
+
- Cleaner syntax when specifying extensions - no need for complex generic type parameters
|
|
421
|
+
- Better IDE support with curried function pattern
|
|
422
|
+
- Arguments are automatically inferred from the definition
|
|
423
|
+
|
|
424
|
+
#### Enhanced `lazy()` API
|
|
425
|
+
|
|
426
|
+
The `lazy` function supports type parameters for lazy-loaded commands with extensions:
|
|
427
|
+
|
|
428
|
+
```ts
|
|
429
|
+
import { lazy } from 'gunshi'
|
|
430
|
+
import { pluginId as dbId } from '@your-company/plugin-database'
|
|
431
|
+
import { pluginId as cacheId } from '@your-company/plugin-cache'
|
|
432
|
+
|
|
433
|
+
import type { DatabaseExtension, PluginId as DbId } from '@your-company/plugin-database'
|
|
434
|
+
import type { CacheExtension, PluginId as CacheId } from '@your-company/plugin-cache'
|
|
435
|
+
|
|
436
|
+
// Type-safe lazy loading with plugin extensions
|
|
437
|
+
const heavyCommand = lazy<{
|
|
438
|
+
extensions: Record<DbId, DatabaseExtension> & Record<CacheId, CacheExtension>
|
|
439
|
+
}>(async () => {
|
|
440
|
+
// Command is loaded only when needed
|
|
441
|
+
return {
|
|
442
|
+
name: 'process',
|
|
443
|
+
run: async ctx => {
|
|
444
|
+
// Extensions are typed even in lazy-loaded commands
|
|
445
|
+
const cached = await ctx.extensions[cacheId]?.get('data')
|
|
446
|
+
if (cached) return cached
|
|
447
|
+
|
|
448
|
+
const result = await ctx.extensions[dbId]?.query('SELECT * FROM data')
|
|
449
|
+
await ctx.extensions[cacheId]?.set('data', result)
|
|
450
|
+
return result
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
})
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
#### New `lazyWithTypes()` API
|
|
457
|
+
|
|
458
|
+
Similar to `defineWithTypes()`, the new `lazyWithTypes()` function provides better type inference for lazy-loaded commands with extensions:
|
|
459
|
+
|
|
460
|
+
<!-- eslint-skip -->
|
|
461
|
+
|
|
462
|
+
```ts
|
|
463
|
+
import { lazyWithTypes } from 'gunshi'
|
|
464
|
+
|
|
465
|
+
type BuildExtensions = {
|
|
466
|
+
logger: { log: (msg: string) => void }
|
|
467
|
+
metrics: { track: (event: string) => void }
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Define args separately for reusability
|
|
471
|
+
const buildArgs = {
|
|
472
|
+
target: { type: 'enum', required: true, choices: ['dev', 'prod'] },
|
|
473
|
+
minify: { type: 'boolean', default: false }
|
|
474
|
+
} as const
|
|
475
|
+
|
|
476
|
+
// Use lazyWithTypes with both args and extensions
|
|
477
|
+
const buildCommand = lazyWithTypes<{
|
|
478
|
+
args: typeof buildArgs
|
|
479
|
+
extensions: BuildExtensions
|
|
480
|
+
}>()(
|
|
481
|
+
async () => {
|
|
482
|
+
// Heavy dependencies loaded only when needed
|
|
483
|
+
const { buildProject } = await import('./heavy-build-utils')
|
|
484
|
+
|
|
485
|
+
return async ctx => {
|
|
486
|
+
// ctx.values inferred from args
|
|
487
|
+
const { target, minify } = ctx.values
|
|
488
|
+
|
|
489
|
+
// Extensions fully typed
|
|
490
|
+
ctx.extensions.logger?.log(`Building for ${target}...`)
|
|
491
|
+
ctx.extensions.metrics?.track('build.started')
|
|
492
|
+
|
|
493
|
+
await buildProject({ target, minify })
|
|
494
|
+
}
|
|
495
|
+
},
|
|
496
|
+
{
|
|
497
|
+
name: 'build',
|
|
498
|
+
description: 'Build the project',
|
|
499
|
+
args: buildArgs
|
|
500
|
+
}
|
|
501
|
+
)
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
Benefits:
|
|
505
|
+
|
|
506
|
+
- Type-safe lazy loading with plugin extensions
|
|
507
|
+
- Separates type declaration from implementation
|
|
508
|
+
- Supports both args and extensions type parameters
|
|
509
|
+
- Maintains all benefits of lazy loading (performance, code splitting)
|
|
510
|
+
|
|
511
|
+
#### Enhanced `cli()` API
|
|
512
|
+
|
|
513
|
+
The `cli` function accepts type parameters for the entire CLI application's extensions:
|
|
514
|
+
|
|
515
|
+
```ts
|
|
516
|
+
import { cli } from 'gunshi'
|
|
517
|
+
import i18n, { pluginId as i18nId } from '@gunshi/plugin-i18n'
|
|
518
|
+
import metrics, { pluginId as metricsId } from '@your-company/plugin-metrics'
|
|
519
|
+
|
|
520
|
+
import type { I18nExtension, PluginId as I18nId } from '@gunshi/plugin-i18n'
|
|
521
|
+
import type { MetricsExtension, PluginId as MetricsId } from '@your-company/plugin-metrics'
|
|
522
|
+
|
|
523
|
+
// Type-safe CLI with multiple plugin extensions
|
|
524
|
+
await cli<{ extensions: Record<I18nId, I18nExtension> & Record<MetricsId, MetricsExtension> }>(
|
|
525
|
+
process.argv.slice(2),
|
|
526
|
+
async ctx => {
|
|
527
|
+
// All plugin extensions are available and typed
|
|
528
|
+
const greeting = ctx.extensions[i18nId]?.translate('welcome')
|
|
529
|
+
console.log(greeting)
|
|
530
|
+
|
|
531
|
+
// Track CLI usage
|
|
532
|
+
ctx.extensions[metricsId]?.track('cli.started', {
|
|
533
|
+
command: ctx.name,
|
|
534
|
+
locale: ctx.extensions[i18nId]?.locale.toString()
|
|
535
|
+
})
|
|
536
|
+
},
|
|
537
|
+
{
|
|
538
|
+
name: 'my-cli',
|
|
539
|
+
plugins: [i18n(), metrics()]
|
|
540
|
+
}
|
|
541
|
+
)
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
These type safety improvements ensure that:
|
|
545
|
+
|
|
546
|
+
- Command arguments are validated at compile time
|
|
547
|
+
- Plugin extensions are properly typed and accessible
|
|
548
|
+
- Typos and incorrect property access are caught before runtime
|
|
549
|
+
- IDE autocompletion works seamlessly across your CLI application
|
|
550
|
+
|
|
551
|
+
## ๐ฅ Breaking Changes
|
|
552
|
+
|
|
553
|
+
### Internationalization migration
|
|
554
|
+
|
|
555
|
+
The built-in i18n support in CLI options has been moved to the official i18n plugin:
|
|
556
|
+
|
|
557
|
+
**Before (v0.26):**
|
|
558
|
+
|
|
559
|
+
```js
|
|
560
|
+
import { cli } from 'gunshi'
|
|
561
|
+
|
|
562
|
+
const command = {
|
|
563
|
+
name: 'app',
|
|
564
|
+
resource: async ctx => {
|
|
565
|
+
// Return translations based on locale
|
|
566
|
+
if (ctx.locale.toString() === 'ja-JP') {
|
|
567
|
+
return { greeting: 'ใใใซใกใฏ' }
|
|
568
|
+
}
|
|
569
|
+
return { greeting: 'Hello' }
|
|
570
|
+
},
|
|
571
|
+
run: ctx => {
|
|
572
|
+
console.log(ctx.translate('greeting'))
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// locale and translationAdapterFactory were in CLI options
|
|
577
|
+
await cli(process.argv.slice(2), command, {
|
|
578
|
+
name: 'my-app',
|
|
579
|
+
version: '1.0.0',
|
|
580
|
+
locale: new Intl.Locale('en-US'),
|
|
581
|
+
translationAdapterFactory: customAdapter
|
|
582
|
+
})
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
**After (v0.27):**
|
|
586
|
+
|
|
587
|
+
```ts
|
|
588
|
+
import { cli } from 'gunshi'
|
|
589
|
+
import i18n, { defineI18nWithTypes, pluginId as i18nId } from '@gunshi/plugin-i18n'
|
|
590
|
+
import type { I18nExtension, PluginId as I18nPluginId } from '@gunshi/plugin-i18n'
|
|
591
|
+
|
|
592
|
+
// Type-safe command with i18n plugin extension
|
|
593
|
+
const command = defineI18nWithTypes<{ extensions: Record<I18nPluginId, I18nExtension> }>()({
|
|
594
|
+
name: 'app',
|
|
595
|
+
// resource function is still used with i18n plugin
|
|
596
|
+
resource: locale => {
|
|
597
|
+
if (locale.toString() === 'ja-JP') {
|
|
598
|
+
return { greeting: 'ใใใซใกใฏ' }
|
|
599
|
+
}
|
|
600
|
+
return { greeting: 'Hello' }
|
|
601
|
+
},
|
|
602
|
+
run: ctx => {
|
|
603
|
+
// Use the translate() function from i18n plugin extension
|
|
604
|
+
console.log(ctx.extensions[i18nId].translate('greeting'))
|
|
605
|
+
}
|
|
606
|
+
})
|
|
607
|
+
|
|
608
|
+
// locale and translationAdapterFactory removed from CLI options
|
|
609
|
+
await cli(process.argv.slice(2), command, {
|
|
610
|
+
name: 'my-app',
|
|
611
|
+
version: '1.0.0',
|
|
612
|
+
plugins: [
|
|
613
|
+
i18n({
|
|
614
|
+
locale: 'en-US', // Moved from CLI options
|
|
615
|
+
translationAdapterFactory: customAdapter // Moved from CLI options
|
|
616
|
+
})
|
|
617
|
+
]
|
|
618
|
+
})
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
**Migration steps:**
|
|
622
|
+
|
|
623
|
+
1. Install the i18n plugin: `npm install --save @gunshi/plugin-i18n`
|
|
624
|
+
2. Import the plugin and its ID: `import i18n, { pluginId as i18nId } from '@gunshi/plugin-i18n'`
|
|
625
|
+
3. Move `locale` from CLI options to i18n plugin options
|
|
626
|
+
4. Move `translationAdapterFactory` from CLI options to i18n plugin options
|
|
627
|
+
5. Change `ctx.translate()` to `ctx.extensions[i18nId].translate()`
|
|
628
|
+
6. Keep using `resource` function in commands
|
|
629
|
+
|
|
630
|
+
**Additional i18n Resources and Helpers:**
|
|
631
|
+
|
|
632
|
+
The i18n plugin comes with additional packages and helper functions to simplify internationalization:
|
|
633
|
+
|
|
634
|
+
- `@gunshi/resources`: Pre-built localization resources for common CLI terms
|
|
635
|
+
|
|
636
|
+
```js
|
|
637
|
+
import resources from '@gunshi/resources'
|
|
638
|
+
|
|
639
|
+
// Use with i18n plugin
|
|
640
|
+
i18n({
|
|
641
|
+
locale: 'en-US',
|
|
642
|
+
builtinResources: resources
|
|
643
|
+
})
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
- **Helper Functions**: Type-safe command definition with i18n
|
|
647
|
+
|
|
648
|
+
```js
|
|
649
|
+
import { defineI18n, withI18nResource } from '@gunshi/plugin-i18n'
|
|
650
|
+
|
|
651
|
+
// Define new command with i18n support
|
|
652
|
+
const command = defineI18n({
|
|
653
|
+
name: 'deploy',
|
|
654
|
+
resource: async ctx => ({
|
|
655
|
+
/* translations */
|
|
656
|
+
}),
|
|
657
|
+
run: ctx => {
|
|
658
|
+
/* command logic */
|
|
659
|
+
}
|
|
660
|
+
})
|
|
661
|
+
|
|
662
|
+
// Add i18n to existing command
|
|
663
|
+
const enhancedCommand = withI18nResource(existingCommand, {
|
|
664
|
+
resource: async ctx => ({
|
|
665
|
+
/* translations */
|
|
666
|
+
})
|
|
667
|
+
})
|
|
668
|
+
```
|
|
669
|
+
|
|
670
|
+
- **Key Resolution Helpers**: Proper namespace management
|
|
671
|
+
|
|
672
|
+
```js
|
|
673
|
+
import { resolveKey } from '@gunshi/plugin-i18n'
|
|
674
|
+
|
|
675
|
+
// In subcommands, use `resolveKey` for proper namespacing
|
|
676
|
+
run: ctx => {
|
|
677
|
+
const key = resolveKey('customMessage', ctx.name)
|
|
678
|
+
const message = ctx.extensions[i18nId].translate(key)
|
|
679
|
+
}
|
|
680
|
+
```
|
|
681
|
+
|
|
682
|
+
## ๐ Documentation
|
|
683
|
+
|
|
684
|
+
### New Plugin Guides
|
|
685
|
+
|
|
686
|
+
- **Plugin Ecosystem Guide**: Comprehensive introduction to the plugin system
|
|
687
|
+
- **Context Extensions Guide**: Learn how to leverage plugin extensions
|
|
688
|
+
- **Plugin Development Guide**: Step-by-step guide for creating custom plugins
|
|
689
|
+
|
|
690
|
+
### Updated API Reference
|
|
691
|
+
|
|
692
|
+
- New plugin-related interfaces: `PluginOptions`, `PluginWithExtension`, `PluginWithoutExtension`
|
|
693
|
+
- Enhanced type definitions: `GunshiParams`, `CommandContextExtension`
|
|
694
|
+
- Rendering options: `RenderingOptions`, `RendererDecorator`
|
|
695
|
+
|
|
696
|
+
### Example Projects
|
|
697
|
+
|
|
698
|
+
New playground examples demonstrating:
|
|
699
|
+
|
|
700
|
+
- Plugin usage patterns
|
|
701
|
+
- Context extension scenarios
|
|
702
|
+
- Advanced type safety with plugins
|
|
703
|
+
|
|
704
|
+
## ๐งโ๐คโ๐ง Contributors
|
|
705
|
+
|
|
706
|
+
We'd like to thank all the contributors who made this release possible:
|
|
707
|
+
|
|
708
|
+
- [@kazupon](https://github.com/kazupon) - Core maintainer, plugin system architect, i18n extraction, lifecycle hooks
|
|
709
|
+
- [@yukukotani](https://github.com/yukukotani) - Fallback to entry command feature (#291)
|
|
710
|
+
- [@sushichan044](https://github.com/sushichan044) - Explicit argument detection feature (#232)
|
|
711
|
+
- [@43081j](https://github.com/43081j) - Exposed `Plugin` type (#159)
|
|
712
|
+
- [@theoephraim](https://github.com/theoephraim) - Documentation improvements (#249)
|
|
713
|
+
- [@lukekarrys](https://github.com/lukekarrys) - Documentation updates (#228)
|
|
714
|
+
- [@BobbieGoede](https://github.com/BobbieGoede) - Fixed FUNDING.yml (#303)
|
|
715
|
+
|
|
716
|
+
Special thanks to the community for feedback and bug reports that helped shape v0.27!
|
|
717
|
+
|
|
718
|
+
## ๐ Credits
|
|
719
|
+
|
|
720
|
+
The completion plugin is powered by:
|
|
721
|
+
|
|
722
|
+
- [`@bomb.sh/tab`](https://github.com/bombshell-dev/tab) by [Bombshell](https://github.com/bombshell-dev) - Shell completion library that powers our tab completion functionality
|