@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
package/LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 kazuya kawaguchi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
6
|
+
this software and associated documentation files (the "Software"), to deal in
|
|
7
|
+
the Software without restriction, including without limitation the rights to
|
|
8
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
9
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
|
10
|
+
subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
17
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
18
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
19
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
20
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@gunshi/docs",
|
|
3
|
+
"description": "Documentation for gunshi",
|
|
4
|
+
"version": "0.27.0-beta.4",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "kazuya kawaguchi",
|
|
7
|
+
"email": "kawakazu80@gmail.com"
|
|
8
|
+
},
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/kazupon/gunshi.git",
|
|
13
|
+
"directory": "packages/docs"
|
|
14
|
+
},
|
|
15
|
+
"type": "module",
|
|
16
|
+
"files": [
|
|
17
|
+
"README.md",
|
|
18
|
+
"src/api/context",
|
|
19
|
+
"src/api/default",
|
|
20
|
+
"src/api/definition",
|
|
21
|
+
"src/api/generator",
|
|
22
|
+
"src/api/plugin",
|
|
23
|
+
"src/api/renderer",
|
|
24
|
+
"src/guide",
|
|
25
|
+
"src/release",
|
|
26
|
+
"src/index.md",
|
|
27
|
+
"src/showcase.md"
|
|
28
|
+
],
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@vueuse/core": "^14.0.0",
|
|
31
|
+
"mermaid": "^11.12.0",
|
|
32
|
+
"oxc-minify": "^0.97.0",
|
|
33
|
+
"typedoc": "^0.28.15",
|
|
34
|
+
"tsx": "^4.21.0",
|
|
35
|
+
"typedoc-plugin-markdown": "^4.9.0",
|
|
36
|
+
"typedoc-vitepress-theme": "^1.1.2",
|
|
37
|
+
"vitepress": "^2.0.0-alpha.12",
|
|
38
|
+
"vitepress-plugin-group-icons": "^1.5.5",
|
|
39
|
+
"vitepress-plugin-llms": "^1.3.4",
|
|
40
|
+
"vitepress-plugin-mermaid": "^2.0.17",
|
|
41
|
+
"vue": "^3.5.14"
|
|
42
|
+
},
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "pnpm run build:typedoc && pnpm run build:readme && pnpm run build:vitepress",
|
|
45
|
+
"build:typedoc": "typedoc --excludeInternal",
|
|
46
|
+
"build:readme": "pnpm run generate",
|
|
47
|
+
"build:vitepress": "vitepress build src",
|
|
48
|
+
"generate": "tsx ./scripts/generate.ts",
|
|
49
|
+
"dev": "pnpm run build:typedoc && vitepress dev src",
|
|
50
|
+
"preview": "vitepress preview src"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
# Advanced Lazy Loading and Sub-Commands
|
|
2
|
+
|
|
3
|
+
This guide explores advanced patterns for implementing lazy loading with sub-commands in Gunshi, based on real-world implementations like [pnpmc](https://github.com/kazupon/pnpmc).
|
|
4
|
+
|
|
5
|
+
## Why Use Advanced Lazy Loading?
|
|
6
|
+
|
|
7
|
+
While Gunshi's basic lazy loading (covered in [Lazy & Async](../essentials/lazy-async.md)) is powerful, large CLI applications with many sub-commands can benefit from more advanced patterns:
|
|
8
|
+
|
|
9
|
+
- **Modular Organization**: Separate commands into independent packages or modules
|
|
10
|
+
- **On-Demand Loading**: Load command implementations only when explicitly invoked
|
|
11
|
+
- **Reduced Memory Footprint**: Minimize memory usage by loading only what's needed
|
|
12
|
+
- **Faster Startup**: Improve CLI startup time by deferring command loading
|
|
13
|
+
- **Better Maintainability**: Isolate command implementations for easier maintenance
|
|
14
|
+
|
|
15
|
+
## Real-World Example: pnpmc Pattern
|
|
16
|
+
|
|
17
|
+
The [pnpmc](https://github.com/kazupon/pnpmc) project (PNPM Catalogs Tooling) demonstrates an effective pattern for organizing a CLI with lazy-loaded sub-commands:
|
|
18
|
+
|
|
19
|
+
1. **Bundled Metadata, Lazy-Loaded Implementations**:
|
|
20
|
+
- Command metadata (name, description, arguments) is imported directly and bundled with the main CLI package
|
|
21
|
+
- Only the command runners (implementations) are lazy-loaded when executed
|
|
22
|
+
- This allows displaying help information for all commands without loading implementations
|
|
23
|
+
|
|
24
|
+
2. **Modular Package Structure**:
|
|
25
|
+
- Command metadata is exposed from separate packages via `meta.js` files and imported directly
|
|
26
|
+
- Command implementations are in separate packages and loaded on-demand
|
|
27
|
+
- This separation enables showing usage via `--help` without loading all command code
|
|
28
|
+
|
|
29
|
+
3. **Custom Loader Implementation**:
|
|
30
|
+
- A custom loader dynamically imports only the command runners when needed
|
|
31
|
+
- Error handling for module resolution failures
|
|
32
|
+
|
|
33
|
+
Let's explore how to implement this pattern in your own CLI applications.
|
|
34
|
+
|
|
35
|
+
## Implementation Pattern
|
|
36
|
+
|
|
37
|
+
### 1. Project Structure
|
|
38
|
+
|
|
39
|
+
For a CLI with multiple sub-commands, consider organizing your code like this:
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
my-cli/
|
|
43
|
+
├── packages/
|
|
44
|
+
│ ├── cli/ # Main CLI package
|
|
45
|
+
│ │ ├── src/
|
|
46
|
+
│ │ │ ├── commands.ts # Command definitions
|
|
47
|
+
│ │ │ ├── loader.ts # Custom loader
|
|
48
|
+
│ │ │ └── cli.ts # CLI entry point
|
|
49
|
+
│ ├── command-a/ # Command A package
|
|
50
|
+
│ │ ├── src/
|
|
51
|
+
│ │ │ ├── meta.ts # Command metadata
|
|
52
|
+
│ │ │ └── runner.ts # Command implementation
|
|
53
|
+
│ └── command-b/ # Command B package
|
|
54
|
+
│ ├── src/
|
|
55
|
+
│ │ ├── meta.ts # Command metadata
|
|
56
|
+
│ │ └── runner.ts # Command implementation
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### 2. Command Metadata
|
|
60
|
+
|
|
61
|
+
Define command metadata in a separate file (e.g., `meta.ts`):
|
|
62
|
+
|
|
63
|
+
```ts [packages/command-a/src/meta.ts]
|
|
64
|
+
import { define } from 'gunshi'
|
|
65
|
+
|
|
66
|
+
export default define({
|
|
67
|
+
name: 'command-a',
|
|
68
|
+
description: 'Performs action A',
|
|
69
|
+
args: {
|
|
70
|
+
input: {
|
|
71
|
+
type: 'string',
|
|
72
|
+
short: 'i',
|
|
73
|
+
description: 'Input file'
|
|
74
|
+
},
|
|
75
|
+
output: {
|
|
76
|
+
type: 'string',
|
|
77
|
+
short: 'o',
|
|
78
|
+
description: 'Output file'
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
})
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### 3. Command Implementation
|
|
85
|
+
|
|
86
|
+
Implement the command in a separate file (e.g., `runner.ts`):
|
|
87
|
+
|
|
88
|
+
```ts [packages/command-a/src/ruuner.ts]
|
|
89
|
+
import meta from './meta.ts'
|
|
90
|
+
import type { CommandRunner } from 'gunshi'
|
|
91
|
+
|
|
92
|
+
export const run: CommandRunner<{ args: typeof meta.args }> = async ctx => {
|
|
93
|
+
const { input, output } = ctx.values
|
|
94
|
+
console.log(`Processing ${input} to ${output}`)
|
|
95
|
+
// Command implementation...
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### 4. Custom Loader
|
|
100
|
+
|
|
101
|
+
Create a custom loader to dynamically import command implementations:
|
|
102
|
+
|
|
103
|
+
```ts [packages/cli/src/loader.ts]
|
|
104
|
+
import type { CommandRunner, GunshiParamsConstraint } from 'gunshi'
|
|
105
|
+
|
|
106
|
+
export async function load<G extends GunshiParamsConstraint>(
|
|
107
|
+
pkg: string
|
|
108
|
+
): Promise<CommandRunner<G> | null> {
|
|
109
|
+
let mod: Promise<CommandRunner<G> | null> | undefined
|
|
110
|
+
// Dynamic import of the command package
|
|
111
|
+
try {
|
|
112
|
+
mod = await import(pkg).then(m => m.default || m)
|
|
113
|
+
} catch (error: unknown) {
|
|
114
|
+
// Handle module not found errors
|
|
115
|
+
if (isErrorModuleNotFound(error)) {
|
|
116
|
+
mod = Promise.resolve(null)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (mod === undefined) {
|
|
120
|
+
throw new Error(`Fatal Error: '${pkg}' Command Runner loading failed`)
|
|
121
|
+
}
|
|
122
|
+
return mod
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function isErrorModuleNotFound(e: unknown): e is NodeJS.ErrnoException {
|
|
126
|
+
return (
|
|
127
|
+
e instanceof Error &&
|
|
128
|
+
'code' in e &&
|
|
129
|
+
typeof e.code === 'string' &&
|
|
130
|
+
e.code === 'ERR_MODULE_NOT_FOUND'
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### 5. Command Definitions
|
|
136
|
+
|
|
137
|
+
Define your commands using Gunshi's `lazy` function and your custom loader:
|
|
138
|
+
|
|
139
|
+
```ts [packages/cli/src/commands.ts]
|
|
140
|
+
import { lazy } from 'gunshi'
|
|
141
|
+
import { load } from './loader.ts'
|
|
142
|
+
|
|
143
|
+
// Import command metadata directly - these are bundled with your CLI
|
|
144
|
+
import metaCommandA from 'command-a/meta'
|
|
145
|
+
import metaCommandB from 'command-b/meta'
|
|
146
|
+
|
|
147
|
+
// Create lazy-loaded commands
|
|
148
|
+
// Note: Only the implementation (runner) is lazy-loaded, not the metadata
|
|
149
|
+
export const commandALazy = lazy(
|
|
150
|
+
// This function is only called when the command is executed
|
|
151
|
+
async () => await load('command-a'),
|
|
152
|
+
// Metadata is provided directly and available immediately
|
|
153
|
+
metaCommandA
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
export const commandBLazy = lazy(async () => await load('command-b'), metaCommandB)
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
This approach ensures that:
|
|
160
|
+
|
|
161
|
+
1. Command metadata is immediately available for generating help text
|
|
162
|
+
2. Command implementations are only loaded when the command is actually executed
|
|
163
|
+
|
|
164
|
+
### 6. CLI Entry Point
|
|
165
|
+
|
|
166
|
+
Set up your CLI entry point to use the lazy-loaded commands:
|
|
167
|
+
|
|
168
|
+
```ts [packages/cli/src/cli.ts]
|
|
169
|
+
import { cli } from 'gunshi'
|
|
170
|
+
import { commandALazy, commandBLazy } from './commands.ts'
|
|
171
|
+
|
|
172
|
+
async function main() {
|
|
173
|
+
// Load package.json for version info
|
|
174
|
+
const { default: pkgJsonModule } = await import('./package.json', { with: { type: 'json' } })
|
|
175
|
+
|
|
176
|
+
// Run the CLI with lazy-loaded commands
|
|
177
|
+
await cli(process.argv.slice(2), commandALazy, {
|
|
178
|
+
name: 'my-cli',
|
|
179
|
+
version: pkgJson.version,
|
|
180
|
+
description: 'My CLI application',
|
|
181
|
+
subCommands: {
|
|
182
|
+
[commandALazy.commandName]: commandALazy,
|
|
183
|
+
[commandBLazy.commandName]: commandBLazy
|
|
184
|
+
}
|
|
185
|
+
})
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
await main()
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## Advanced Techniques
|
|
192
|
+
|
|
193
|
+
### On-Demand Sub-Command Loading
|
|
194
|
+
|
|
195
|
+
For CLIs with many sub-commands, you can implement on-demand sub-command loading:
|
|
196
|
+
|
|
197
|
+
```ts [packages/cli/src/commands.ts]
|
|
198
|
+
import { lazy } from 'gunshi/definition'
|
|
199
|
+
import { load } from './loader.ts'
|
|
200
|
+
|
|
201
|
+
// Function to create a lazy command
|
|
202
|
+
function createLazyCommand(name: string) {
|
|
203
|
+
return lazy(
|
|
204
|
+
async () => {
|
|
205
|
+
// Dynamically import metadata and implementation
|
|
206
|
+
const meta = await import(`${name}/meta`).then(m => m.default || m)
|
|
207
|
+
return await load(name)
|
|
208
|
+
},
|
|
209
|
+
{ name } // Minimal metadata, rest will be loaded on demand
|
|
210
|
+
)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Create commands map with factory function
|
|
214
|
+
export const commands = new Map([
|
|
215
|
+
['command-a', createLazyCommand('command-a')],
|
|
216
|
+
['command-b', createLazyCommand('command-b')]
|
|
217
|
+
// Add more commands as needed
|
|
218
|
+
])
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Package Manager Integration
|
|
222
|
+
|
|
223
|
+
For CLI tools that integrate with package managers (like pnpmc does with pnpm), you can enhance your loader:
|
|
224
|
+
|
|
225
|
+
```ts [packages/cli/src/loader.ts]
|
|
226
|
+
import { detect, resolveCommand } from 'package-manager-detector'
|
|
227
|
+
import { x } from 'tinyexec'
|
|
228
|
+
import type { CommandContext, CommandRunner, GunshiParamsConstraint } from 'gunshi'
|
|
229
|
+
|
|
230
|
+
export async function load<G extends GunshiParamsConstraint>(
|
|
231
|
+
pkg: string
|
|
232
|
+
): Promise<CommandRunner<G>> {
|
|
233
|
+
// Detect package manager (npm, yarn, pnpm, etc.)
|
|
234
|
+
const pm = await detect()
|
|
235
|
+
if (pm === null) {
|
|
236
|
+
throw new Error('Fatal Error: Cannot detect package manager')
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Return a command runner function
|
|
240
|
+
async function runner<G extends GunshiParamsConstraint>(ctx: CommandContext<G>): Promise<void> {
|
|
241
|
+
// Construct the sub-command
|
|
242
|
+
const subCommand = ctx.env.version ? `${pkg}@${ctx.env.version}` : pkg
|
|
243
|
+
|
|
244
|
+
// Resolve the command using the package manager
|
|
245
|
+
const resolvedCommand = resolveCommand(pm.agent, 'execute', [subCommand, ...ctx._.slice(1)])
|
|
246
|
+
if (resolvedCommand === null) {
|
|
247
|
+
throw new Error(`Fatal Error: Cannot resolve command '${ctx._[0]}'`)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Execute the command
|
|
251
|
+
await x(resolvedCommand.command, resolvedCommand.args, {
|
|
252
|
+
nodeOptions: {
|
|
253
|
+
cwd: ctx.env.cwd,
|
|
254
|
+
stdio: 'inherit',
|
|
255
|
+
env: Object.assign({}, process.env, { CLI_LOADER: 'true' })
|
|
256
|
+
}
|
|
257
|
+
})
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return runner
|
|
261
|
+
}
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
## Performance Considerations
|
|
265
|
+
|
|
266
|
+
When implementing advanced lazy loading, consider these performance optimizations:
|
|
267
|
+
|
|
268
|
+
1. **Metadata Size**: Keep command metadata small since it's bundled with your CLI
|
|
269
|
+
2. **Metadata/Implementation Separation**: Clearly separate what's needed for help text vs. execution
|
|
270
|
+
3. **Dependency Management**: Keep implementation dependencies isolated to each command package
|
|
271
|
+
4. **Caching**: Cache loaded command implementations to avoid repeated imports
|
|
272
|
+
5. **Error Handling**: Implement robust error handling for implementation loading failures
|
|
273
|
+
6. **Startup Time**: Measure and optimize CLI startup time by minimizing what's loaded initially
|
|
274
|
+
|
|
275
|
+
## Type Safety
|
|
276
|
+
|
|
277
|
+
Maintain type safety with TypeScript when implementing advanced lazy loading:
|
|
278
|
+
|
|
279
|
+
```ts [packages/cli/src/commands.ts]
|
|
280
|
+
import { define, lazyWithTypes } from 'gunshi/definition'
|
|
281
|
+
import { load } from './loader.ts'
|
|
282
|
+
import type { CommandRunner } from 'gunshi'
|
|
283
|
+
|
|
284
|
+
// Define command metadata with type safety
|
|
285
|
+
const metaCommandA = define({
|
|
286
|
+
name: 'command-a',
|
|
287
|
+
description: 'Performs action A',
|
|
288
|
+
args: {
|
|
289
|
+
input: {
|
|
290
|
+
type: 'string',
|
|
291
|
+
short: 'i',
|
|
292
|
+
description: 'Input file'
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
// Create type-safe lazy command
|
|
298
|
+
const commandALazy = lazyWithTypes<{ args: typeof metaCommandA.args }>()(
|
|
299
|
+
async () => await load('command-a'),
|
|
300
|
+
metaCommandA
|
|
301
|
+
)
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
## Conclusion
|
|
305
|
+
|
|
306
|
+
Advanced lazy loading with sub-commands allows you to build scalable, maintainable CLI applications with optimal performance. By bundling command metadata with your main CLI while lazy-loading command implementations, you can create complex CLIs that:
|
|
307
|
+
|
|
308
|
+
1. Start up quickly with minimal initial loading
|
|
309
|
+
2. Display comprehensive help information for all commands
|
|
310
|
+
3. Only load command implementations when they're actually executed
|
|
311
|
+
|
|
312
|
+
The pattern demonstrated by pnpmc provides a solid foundation for organizing your CLI code, which you can adapt and extend to meet your specific requirements.
|