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