@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,519 @@
1
+ # Plugin Dependencies
2
+
3
+ Gunshi's plugin system includes a sophisticated dependency management system that ensures plugins load in the correct order and can safely interact with each other.
4
+
5
+ This guide covers everything you need to know about plugin dependencies.
6
+
7
+ ## Understanding Plugin Dependencies
8
+
9
+ Plugin dependencies allow you to:
10
+
11
+ - Ensure required plugins are loaded before your plugin
12
+ - Access functionality from other plugins
13
+ - Build composable plugin ecosystems
14
+ - Handle optional features gracefully
15
+
16
+ ## Why Declare Dependencies?
17
+
18
+ Declaring dependencies explicitly provides several benefits:
19
+
20
+ 1. **Load Order Guarantee**: Ensures your plugin's dependencies are initialized before your plugin runs
21
+ 2. **Runtime Safety**: Prevents runtime errors from missing required functionality
22
+ 3. **Clear Documentation**: Makes plugin relationships explicit and discoverable
23
+ 4. **Type Safety**: Enables TypeScript to validate extension availability at compile time (see [Type-Safe Dependencies](./type-system.md#plugin-with-dependencies))
24
+ 5. **Error Prevention**: Gunshi can detect missing dependencies and provide helpful error messages
25
+
26
+ ## Dependency Resolution Process
27
+
28
+ Gunshi uses **topological sorting** to resolve plugin dependencies, ensuring that:
29
+
30
+ 1. Plugins with no dependencies load first
31
+ 2. Dependent plugins load after their dependencies
32
+ 3. Circular dependencies are detected and prevented
33
+
34
+ ### Example Dependency Graph
35
+
36
+ Consider the following plugin dependency relationships:
37
+
38
+ ```mermaid
39
+ graph LR
40
+ A[Logger Plugin]
41
+ B[Cache Plugin] --> A
42
+ C[Auth Plugin] --> A
43
+ C --> B
44
+ D[API Plugin] --> C
45
+
46
+ style A fill:#4A90E2,stroke:#2E5A8E,stroke-width:2px,color:#fff
47
+ style B fill:#9B59B6,stroke:#633974,stroke-width:2px,color:#fff
48
+ style C fill:#468c56,stroke:#2e5936,stroke-width:2px,color:#fff
49
+ style D fill:#E67E22,stroke:#935014,stroke-width:2px,color:#fff
50
+ ```
51
+
52
+ In this dependency graph:
53
+
54
+ - **Logger Plugin** has no dependencies (it's a base plugin)
55
+ - **Cache Plugin** depends on Logger Plugin (needs logging functionality)
56
+ - **Auth Plugin** depends on both Logger Plugin and Cache Plugin (needs logging and caching)
57
+ - **API Plugin** depends on Auth Plugin (requires authenticated users)
58
+
59
+ ### Resolution Order
60
+
61
+ Based on the dependency graph above, Gunshi's topological sorting algorithm determines the following loading order:
62
+
63
+ **Loading order: Logger → Cache → Auth → API**
64
+
65
+ This ensures that:
66
+
67
+ 1. **Logger** loads first (no dependencies)
68
+ 2. **Cache** loads after Logger (its dependency is satisfied)
69
+ 3. **Auth** loads after both Logger and Cache (both dependencies are satisfied)
70
+ 4. **API** loads last after Auth (its dependency is satisfied)
71
+
72
+ Note that Logger and Cache must both be loaded before Auth can initialize, as Auth depends on both of them.
73
+
74
+ ## Declaring Dependencies
75
+
76
+ Plugin dependencies are declared in the plugin configuration using the `dependencies` property.
77
+
78
+ This property accepts an array of dependency specifications that tell Gunshi which other plugins must be loaded before your plugin can function correctly.
79
+
80
+ ### Dependency Declaration Syntax
81
+
82
+ Dependencies can be declared in two ways:
83
+
84
+ ```js
85
+ // Simple string format for required dependencies
86
+ dependencies: ['logger', 'auth']
87
+
88
+ // Object format for optional dependencies
89
+ dependencies: [
90
+ 'logger', // Required dependency
91
+ { id: 'cache', optional: true } // Optional dependency
92
+ ]
93
+ ```
94
+
95
+ ### Required Dependencies
96
+
97
+ Required dependencies must be present for your plugin to load. If a required dependency is missing, Gunshi will throw an error during initialization.
98
+
99
+ <!-- eslint-disable markdown/no-missing-label-refs -->
100
+
101
+ > [!WARNING]
102
+ > If you register multiple plugins with the same ID, Gunshi will emit a warning: `Duplicate plugin id detected`. While the plugins will still load, having duplicate IDs can lead to unexpected behavior when accessing extensions or resolving dependencies. Always ensure each plugin has a unique ID.
103
+
104
+ <!-- eslint-enable markdown/no-missing-label-refs -->
105
+
106
+ Declare required dependencies using the `dependencies` array:
107
+
108
+ ```js
109
+ import { plugin } from 'gunshi/plugin'
110
+
111
+ // Simple string dependency
112
+ const auth = plugin({
113
+ id: 'auth',
114
+ dependencies: ['logger'], // Requires 'logger' plugin
115
+ setup: ctx => {
116
+ // Logger plugin is guaranteed to be loaded
117
+ }
118
+ })
119
+
120
+ // Multiple dependencies
121
+ const api = plugin({
122
+ id: 'api',
123
+ dependencies: ['auth', 'cache', 'logger'],
124
+ setup: ctx => {
125
+ // All three plugins are loaded
126
+ }
127
+ })
128
+ ```
129
+
130
+ ### Optional Dependencies
131
+
132
+ Optional dependencies allow your plugin to enhance its functionality when certain plugins are available, while still functioning correctly when they're not.
133
+
134
+ This enables graceful degradation and flexible plugin ecosystems.
135
+
136
+ #### When to Use Optional Dependencies
137
+
138
+ Use optional dependencies when:
139
+
140
+ - Your plugin can provide additional features with another plugin, but doesn't require it
141
+ - You want to support multiple plugin configurations
142
+ - You're building plugins that adapt to different environments
143
+ - You need to maintain backward compatibility
144
+
145
+ #### Declaring Optional Dependencies
146
+
147
+ Mark dependencies as optional using the object format:
148
+
149
+ ```js
150
+ import { plugin } from 'gunshi/plugin'
151
+
152
+ const enhanced = plugin({
153
+ id: 'enhanced',
154
+ dependencies: [
155
+ 'core', // Required
156
+ { id: 'cache', optional: true }, // Optional
157
+ { id: 'metrics', optional: true } // Optional
158
+ ],
159
+ setup: ctx => {
160
+ // 'core' is guaranteed
161
+ // 'cache' and 'metrics' might not be present
162
+ }
163
+ })
164
+ ```
165
+
166
+ ## Circular Dependencies
167
+
168
+ A circular dependency occurs when two or more plugins depend on each other, creating a dependency loop that cannot be resolved.
169
+
170
+ Gunshi's dependency resolution system detects these cycles and prevents them to ensure a stable plugin initialization order.
171
+
172
+ ### Understanding Circular Dependencies
173
+
174
+ Circular dependencies create logical paradoxes in the loading order:
175
+
176
+ - Plugin A requires Plugin B to be loaded first
177
+ - Plugin B requires Plugin A to be loaded first
178
+ - Neither can be loaded before the other
179
+
180
+ This situation makes it impossible to determine a valid initialization sequence and indicates architectural issues such as tight coupling and reduced reusability.
181
+
182
+ ### Detection and Prevention
183
+
184
+ Gunshi automatically detects circular dependencies during the resolution phase and will throw an error:
185
+
186
+ ```js
187
+ import { plugin } from 'gunshi/plugin'
188
+
189
+ // This will fail!
190
+ const pluginA = plugin({
191
+ id: 'A',
192
+ dependencies: ['B'],
193
+ setup: ctx => {}
194
+ })
195
+
196
+ const pluginB = plugin({
197
+ id: 'B',
198
+ dependencies: ['A'], // Circular!
199
+ setup: ctx => {}
200
+ })
201
+
202
+ // Circular dependency detected: `a -> b -> a`
203
+ ```
204
+
205
+ Circular dependencies can also occur in longer chains:
206
+
207
+ ```js
208
+ import { plugin } from 'gunshi/plugin'
209
+
210
+ // Three-way circular dependency
211
+ const pluginX = plugin({
212
+ id: 'X',
213
+ dependencies: ['Y'],
214
+ setup: ctx => {}
215
+ })
216
+
217
+ const pluginY = plugin({
218
+ id: 'Y',
219
+ dependencies: ['Z'],
220
+ setup: ctx => {}
221
+ })
222
+
223
+ const pluginZ = plugin({
224
+ id: 'Z',
225
+ dependencies: ['X'], // Creates cycle: X → Y → Z → X
226
+ setup: ctx => {}
227
+ })
228
+
229
+ // Circular dependency detected: `x -> y -> z -> x`
230
+ ```
231
+
232
+ ### Resolving Circular Dependencies
233
+
234
+ The most practical and recommended approach to resolve circular dependencies is to extract common functionality into a separate plugin.
235
+
236
+ This creates a clean architecture where both plugins can depend on the shared functionality without depending on each other.
237
+
238
+ When two plugins need to share functionality, extract that functionality into a base plugin that both can depend on:
239
+
240
+ **Problem: Circular dependency between two plugins**
241
+
242
+ ```js
243
+ import { plugin } from 'gunshi/plugin'
244
+
245
+ // ❌ Circular dependency - This will fail!
246
+ const pluginA = plugin({
247
+ id: 'plugin-a',
248
+ dependencies: ['plugin-b'], // A needs B
249
+ extension: ctx => ({
250
+ methodA: () => {
251
+ // Uses B's functionality
252
+ return ctx.extensions['plugin-b'].methodB() + ' from A'
253
+ }
254
+ })
255
+ })
256
+
257
+ const pluginB = plugin({
258
+ id: 'plugin-b',
259
+ dependencies: ['plugin-a'], // B needs A
260
+ extension: ctx => ({
261
+ methodB: () => {
262
+ // Uses A's functionality
263
+ return ctx.extensions['plugin-a'].methodA() + ' from B'
264
+ }
265
+ })
266
+ })
267
+ // Circular dependency detected: `plugin-a -> plugin-b -> plugin-a`
268
+ ```
269
+
270
+ **Solution: Extract shared functionality into a common plugin**
271
+
272
+ ```js
273
+ import { plugin, cli } from 'gunshi/plugin'
274
+
275
+ // ✅ Create a common base plugin with shared functionality
276
+ const shared = plugin({
277
+ id: 'shared',
278
+ extension: () => ({
279
+ // Shared state and functionality
280
+ data: { value: 0 },
281
+ increment: function () {
282
+ this.data.value++
283
+ },
284
+ getValue: function () {
285
+ return this.data.value
286
+ }
287
+ })
288
+ })
289
+
290
+ // Plugin A now depends only on shared
291
+ const pluginA = plugin({
292
+ id: 'plugin-a',
293
+ dependencies: ['shared'],
294
+ extension: ctx => ({
295
+ methodA: () => {
296
+ ctx.extensions.shared.increment()
297
+ return `A: value is ${ctx.extensions.shared.getValue()}`
298
+ }
299
+ })
300
+ })
301
+
302
+ // Plugin B also depends only on shared
303
+ const pluginB = plugin({
304
+ id: 'plugin-b',
305
+ dependencies: ['shared'],
306
+ extension: ctx => ({
307
+ methodB: () => {
308
+ const value = ctx.extensions.shared.getValue()
309
+ return `B: current value is ${value}`
310
+ }
311
+ })
312
+ })
313
+
314
+ // Usage - no circular dependency!
315
+ await cli(args, command, {
316
+ plugins: [
317
+ shared, // Loads first
318
+ pluginA, // Loads second (depends on shared)
319
+ pluginB // Loads third (depends on shared)
320
+ ]
321
+ })
322
+ ```
323
+
324
+ This approach offers several benefits:
325
+
326
+ 1. **Clear dependency hierarchy**: shared → pluginA/pluginB (no cycles)
327
+ 2. **Single responsibility**: Each plugin has a focused purpose
328
+ 3. **Reusability**: The shared plugin can be used by other plugins
329
+ 4. **Testability**: Each plugin can be tested independently
330
+ 5. **Maintainability**: Changes to shared logic are centralized
331
+
332
+ ## Complete Dependency Resolution Example
333
+
334
+ Here's a complete example demonstrating dependency resolution order with complex dependencies:
335
+
336
+ logger plugin:
337
+
338
+ ```js [logger.js]
339
+ import { plugin } from 'gunshi/plugin'
340
+
341
+ // Base plugin with no dependencies
342
+ export default plugin({
343
+ id: 'logger',
344
+ setup: ctx => {
345
+ console.log('1. Logger plugin loaded')
346
+ },
347
+ extension: () => ({
348
+ log: msg => console.log(`[LOG] ${msg}`)
349
+ })
350
+ })
351
+ ```
352
+
353
+ cache plugin:
354
+
355
+ ```js [cache.js]
356
+ import { plugin } from 'gunshi/plugin'
357
+
358
+ // Plugin with one required dependency
359
+ export default plugin({
360
+ id: 'cache',
361
+ dependencies: ['logger'],
362
+ setup: ctx => {
363
+ console.log('2. Cache plugin loaded (depends on logger)')
364
+ },
365
+ extension: ctx => ({
366
+ get: key => {
367
+ ctx.extensions.logger.log(`Cache get: ${key}`)
368
+ return null
369
+ }
370
+ })
371
+ })
372
+ ```
373
+
374
+ auth plugin:
375
+
376
+ ```js [auth.js]
377
+ import { plugin } from 'gunshi/plugin'
378
+
379
+ // Plugin with multiple dependencies
380
+ export default plugin({
381
+ id: 'auth',
382
+ dependencies: ['logger', 'cache'],
383
+ setup: ctx => {
384
+ console.log('3. Auth plugin loaded (depends on logger, cache)')
385
+ },
386
+ extension: ctx => ({
387
+ isAuthenticated: () => {
388
+ ctx.extensions.logger.log('Checking authentication')
389
+ ctx.extensions.cache.get('auth-token')
390
+ return true
391
+ }
392
+ })
393
+ })
394
+ ```
395
+
396
+ metrics plugin:
397
+
398
+ ```js [metrics.js]
399
+ import { plugin } from 'gunshi/plugin'
400
+
401
+ // Plugin with optional dependency
402
+ export default plugin({
403
+ id: 'metrics',
404
+ dependencies: ['logger', { id: 'cache', optional: true }],
405
+ setup: ctx => {
406
+ console.log('4. Metrics plugin loaded (depends on logger, optionally cache)')
407
+ },
408
+ extension: ctx => ({
409
+ track: event => {
410
+ ctx.extensions.logger.log(`Tracking: ${event}`)
411
+ // Use cache if available
412
+ if (ctx.extensions.cache) {
413
+ ctx.extensions.cache.get(`metrics:${event}`)
414
+ }
415
+ }
416
+ })
417
+ })
418
+ ```
419
+
420
+ api plugin:
421
+
422
+ ```js [api.js]
423
+ import { plugin } from 'gunshi/plugin'
424
+
425
+ // Plugin that depends on other dependent plugins
426
+ export default plugin({
427
+ id: 'api',
428
+ dependencies: ['auth', 'metrics'],
429
+ setup: ctx => {
430
+ console.log('5. API plugin loaded (depends on auth, metrics)')
431
+ },
432
+ extension: ctx => ({
433
+ request: endpoint => {
434
+ if (ctx.extensions.auth.isAuthenticated()) {
435
+ ctx.extensions.metrics.track(`api:${endpoint}`)
436
+ return { success: true }
437
+ }
438
+ return { success: false }
439
+ }
440
+ })
441
+ })
442
+ ```
443
+
444
+ Last, install all plugins on CLI application:
445
+
446
+ ```js [cli.js]
447
+ import { cli, define } from 'gunshi'
448
+ import logger from './logger.js'
449
+ import cache from './cache.js'
450
+ import auth from './auth.js'
451
+ import metrics from './metrics.js'
452
+ import api from './api.js'
453
+
454
+ // Command to demonstrate plugin loading
455
+ const command = define({
456
+ name: 'demo',
457
+ run: ctx => {
458
+ console.log('\n=== Command execution starts ===')
459
+
460
+ // Use various plugin extensions
461
+ ctx.extensions.logger.log('Command running')
462
+ ctx.extensions.api.request('/users')
463
+
464
+ console.log('=== Command execution ends ===')
465
+ }
466
+ })
467
+
468
+ // Run with plugins in random order - Gunshi will resolve correct order
469
+ await cli(process.argv.slice(2), command, {
470
+ plugins: [
471
+ // Intentionally provide in wrong order
472
+ api, // Depends on auth, metrics
473
+ auth, // Depends on logger, cache
474
+ metrics, // Depends on logger, optionally cache
475
+ logger, // No dependencies
476
+ cache // Depends on logger
477
+ ]
478
+ })
479
+ ```
480
+
481
+ <!-- eslint-disable markdown/no-missing-label-refs -->
482
+
483
+ > [!TIP]
484
+ > The example fully code is [here](https://github.com/kazupon/gunshi/tree/main/playground/plugins/dependencies).
485
+
486
+ <!-- eslint-enable markdown/no-missing-label-refs -->
487
+
488
+ Run your application with plugin:
489
+
490
+ ```sh
491
+ node cli.js
492
+ 1. Logger plugin loaded
493
+ 2. Cache plugin loaded (depends on logger)
494
+ 3. Auth plugin loaded (depends on logger, cache)
495
+ 4. Metrics plugin loaded (depends on logger, optionally cache)
496
+ 5. API plugin loaded (depends on auth, metrics)
497
+
498
+ === Command execution starts ===
499
+ [LOG] Command running
500
+ [LOG] Checking authentication
501
+ [LOG] Cache get: auth-token
502
+ [LOG] Tracking: api:/users
503
+ [LOG] Cache get: metrics:api:/users
504
+ === Command execution ends ===
505
+ ```
506
+
507
+ This example demonstrates:
508
+
509
+ 1. **Topological sorting**: Despite plugins being provided in wrong order, Gunshi resolves them correctly
510
+ 2. **Dependency chain**: `api` → `auth` → `cache` → `logger` shows multi-level dependencies
511
+ 3. **Optional dependencies**: `metrics` plugin works with or without `cache`
512
+ 4. **Load order verification**: Setup messages show the actual resolution order
513
+ 5. **Runtime interaction**: Extensions can access their dependencies safely
514
+
515
+ ## Next Steps
516
+
517
+ You've learned how to manage plugin dependencies, including topological sorting, optional dependencies, and runtime interaction patterns. This knowledge enables you to build sophisticated plugin ecosystems where plugins collaborate effectively.
518
+
519
+ Next, dive into [Plugin Decorators](./decorators.md) to learn how plugins can wrap and enhance existing functionality, adding behaviors like authentication, logging, and caching to commands without modifying their core implementation.