@zudojs/plugins 0.1.0 → 1.0.0

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.
Files changed (65) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +130 -29
  3. package/dist/index.d.ts +34 -13
  4. package/dist/index.js +28 -10
  5. package/dist/pluginDependencies/dependencyResolver.core.d.ts +47 -6
  6. package/dist/pluginDependencies/dependencyResolver.core.js +108 -46
  7. package/dist/pluginDependencies/index.d.ts +6 -2
  8. package/dist/pluginDependencies/index.js +4 -1
  9. package/dist/pluginDependencies/versionCheck.core.d.ts +67 -0
  10. package/dist/pluginDependencies/versionCheck.core.js +167 -0
  11. package/dist/pluginDiagnostics/pluginDiagnostic.core.d.ts +9 -0
  12. package/dist/pluginDiagnostics/pluginDiagnostic.core.js +16 -12
  13. package/dist/pluginIntegration/index.d.ts +2 -1
  14. package/dist/pluginIntegration/index.js +1 -1
  15. package/dist/pluginIntegration/pluginContext.core.d.ts +35 -5
  16. package/dist/pluginIntegration/pluginContext.core.js +28 -5
  17. package/dist/pluginLifecycle/pluginLifecycle.core.d.ts +42 -0
  18. package/dist/pluginLifecycle/pluginLifecycle.core.js +132 -70
  19. package/dist/pluginManager/pluginManager.core.d.ts +121 -3
  20. package/dist/pluginManager/pluginManager.core.js +271 -47
  21. package/dist/pluginRegistry/pluginRegistry.core.d.ts +15 -6
  22. package/dist/pluginRegistry/pluginRegistry.core.js +6 -1
  23. package/dist/pluginTypes/index.d.ts +1 -1
  24. package/dist/pluginTypes/pluginContext.type.d.ts +11 -3
  25. package/dist/pluginTypes/pluginState.type.js +6 -3
  26. package/package.json +22 -15
  27. package/dist/.tsbuildinfo +0 -1
  28. package/dist/index.d.ts.map +0 -1
  29. package/dist/index.js.map +0 -1
  30. package/dist/pluginDependencies/dependencyResolver.core.d.ts.map +0 -1
  31. package/dist/pluginDependencies/dependencyResolver.core.js.map +0 -1
  32. package/dist/pluginDependencies/index.d.ts.map +0 -1
  33. package/dist/pluginDependencies/index.js.map +0 -1
  34. package/dist/pluginDiagnostics/index.d.ts.map +0 -1
  35. package/dist/pluginDiagnostics/index.js.map +0 -1
  36. package/dist/pluginDiagnostics/pluginDiagnostic.core.d.ts.map +0 -1
  37. package/dist/pluginDiagnostics/pluginDiagnostic.core.js.map +0 -1
  38. package/dist/pluginEvents/index.d.ts.map +0 -1
  39. package/dist/pluginEvents/index.js.map +0 -1
  40. package/dist/pluginEvents/pluginEvent.core.d.ts.map +0 -1
  41. package/dist/pluginEvents/pluginEvent.core.js.map +0 -1
  42. package/dist/pluginIntegration/index.d.ts.map +0 -1
  43. package/dist/pluginIntegration/index.js.map +0 -1
  44. package/dist/pluginIntegration/pluginContext.core.d.ts.map +0 -1
  45. package/dist/pluginIntegration/pluginContext.core.js.map +0 -1
  46. package/dist/pluginLifecycle/pluginLifecycle.core.d.ts.map +0 -1
  47. package/dist/pluginLifecycle/pluginLifecycle.core.js.map +0 -1
  48. package/dist/pluginManager/pluginManager.core.d.ts.map +0 -1
  49. package/dist/pluginManager/pluginManager.core.js.map +0 -1
  50. package/dist/pluginRegistry/index.d.ts.map +0 -1
  51. package/dist/pluginRegistry/index.js.map +0 -1
  52. package/dist/pluginRegistry/pluginRegistry.core.d.ts.map +0 -1
  53. package/dist/pluginRegistry/pluginRegistry.core.js.map +0 -1
  54. package/dist/pluginTypes/index.d.ts.map +0 -1
  55. package/dist/pluginTypes/index.js.map +0 -1
  56. package/dist/pluginTypes/plugin.type.d.ts.map +0 -1
  57. package/dist/pluginTypes/plugin.type.js.map +0 -1
  58. package/dist/pluginTypes/pluginContext.type.d.ts.map +0 -1
  59. package/dist/pluginTypes/pluginContext.type.js.map +0 -1
  60. package/dist/pluginTypes/pluginDependency.type.d.ts.map +0 -1
  61. package/dist/pluginTypes/pluginDependency.type.js.map +0 -1
  62. package/dist/pluginTypes/pluginMetadata.type.d.ts.map +0 -1
  63. package/dist/pluginTypes/pluginMetadata.type.js.map +0 -1
  64. package/dist/pluginTypes/pluginState.type.d.ts.map +0 -1
  65. package/dist/pluginTypes/pluginState.type.js.map +0 -1
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Zudojs Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, 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,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,15 +1,16 @@
1
1
  # @zudojs/plugins
2
2
 
3
- Plugin manager, registry, dependency resolver, lifecycle controller, events, and integration context. The basis for extending a Zudojs app with third-party functionality.
3
+ Plugin manager, registry, dependency resolver, lifecycle controller, events, and diagnostics. The basis for extending a Zudojs app with third-party functionality.
4
4
 
5
5
  ## When to use
6
6
 
7
7
  Import this when you need:
8
8
 
9
- - load a plugin from a manifest or package
10
- - resolve plugin dependencies before activation
11
- - run plugin lifecycle hooks (install → enable → disable → uninstall)
12
- - isolate plugin state via a `PluginContext`
9
+ - register extensions and bring them up in dependency order
10
+ - enforce declared dependency versions before anything starts
11
+ - run plugin lifecycle hooks (`install` → `initialize` → `start` → `stop` → `dispose`)
12
+ - give each plugin a scoped `PluginContext` with its own disposables and abort signal
13
+ - roll back cleanly when one plugin fails to start
13
14
 
14
15
  ## Installation
15
16
 
@@ -17,41 +18,141 @@ Import this when you need:
17
18
  npm install @zudojs/plugins
18
19
  ```
19
20
 
20
- ## Public API
21
+ ## Quick start
21
22
 
22
23
  ```typescript
23
- import {
24
- PluginManager,
25
- PluginRegistryImpl,
26
- PluginLifecycleController,
27
- createPluginContext,
28
- createEventEmitter,
29
- type PluginDefinition,
30
- type PluginMetadata,
31
- type PluginContext,
32
- type PluginLifecycleEvent,
33
- type PluginHook,
34
- type DependencyResolution,
35
- type CreatePluginContextOptions,
36
- type PluginHooks,
37
- } from "@zudojs/plugins";
24
+ import { PluginManager, createPluginContext } from "@zudojs/plugins";
25
+
26
+ const manager = new PluginManager({ hookTimeout: 5_000 });
27
+
28
+ manager.register({
29
+ metadata: { name: "@acme/db", version: "1.0.0" },
30
+ async start(context) {
31
+ const pool = openPool();
32
+ // Released automatically, in reverse registration order, on dispose.
33
+ context.registerDisposable({ dispose: () => pool.end() });
34
+ },
35
+ });
36
+
37
+ manager.register({
38
+ metadata: { name: "@acme/api" },
39
+ dependencies: [{ name: "@acme/db", version: "^1.0.0" }],
40
+ async start(context) {
41
+ context.logger?.info("api started");
42
+ },
43
+ });
44
+
45
+ const context = createPluginContext({ name: "@acme/host" });
46
+
47
+ await manager.start(context); // @acme/db first, then @acme/api
48
+ await manager.stop(context); // reverse order; disposables released
38
49
  ```
39
50
 
40
- ## Usage
51
+ `createPluginContext` takes the host's own `PluginMetadata` and, optionally,
52
+ the services plugins may reach (`container`, `config`, `logger`, `events`).
53
+ The manager derives a per-plugin view of it: `context.plugin` names the
54
+ plugin currently running, `context.registerDisposable` and
55
+ `context.onDispose` write to that plugin's own cleanup list, and
56
+ `context.signal` is aborted when the plugin system shuts down.
57
+
58
+ ## Lifecycle
59
+
60
+ | Phase | Called by | Runs |
61
+ | --- | --- | --- |
62
+ | `install(context, options)` | `manager.start` | once, in dependency order |
63
+ | `initialize(context)` | `manager.start` | after every plugin is installed |
64
+ | `start(context)` | `manager.start` | after every plugin is initialized |
65
+ | `stop(context)` | `manager.stop` | reverse dependency order |
66
+ | `dispose(context)` | `manager.stop` | after `stop`, releasing disposables |
67
+
68
+ Every hook is optional. Each phase completes across all plugins before the
69
+ next begins, so a plugin may rely on its dependencies being installed by
70
+ the time its own `initialize` runs.
71
+
72
+ If any phase throws, `manager.start` stops and disposes everything it had
73
+ already brought up, then rethrows. `manager.stop` continues past a failing
74
+ plugin so one bad `stop` cannot strand the rest; those failures are
75
+ reported through the `onError` option (and logged to `console.error` if you
76
+ do not supply one) rather than swallowed.
77
+
78
+ ## Dependency order and versions
79
+
80
+ Required dependencies must be registered, or `start` throws
81
+ `PluginDependencyError` naming both the plugin that declared the dependency
82
+ and the dependency itself. Cycles throw `PluginDependencyCycleError` with
83
+ the cycle path. Optional dependencies that are present still participate in
84
+ ordering; optional dependencies that are absent are ignored.
85
+
86
+ Declared versions are enforced before any hook runs:
41
87
 
42
88
  ```typescript
43
- import { PluginManager, createPluginContext } from "@zudojs/plugins";
89
+ manager.register({ metadata: { name: "@acme/db", version: "1.4.0" } });
90
+ manager.register({
91
+ metadata: { name: "@acme/api" },
92
+ dependencies: [{ name: "@acme/db", version: "^2.0.0" }],
93
+ });
44
94
 
45
- const ctx = createPluginContext({ logger, config });
46
- const manager = new PluginManager(ctx);
95
+ await manager.start(context);
96
+ // PluginDependencyVersionError: Plugin "@acme/api" requires "@acme/db@^2.0.0",
97
+ // but version 1.4.0 is registered. Register a "@acme/db" that satisfies
98
+ // ^2.0.0, relax the constraint on "@acme/api", or construct the manager
99
+ // with { checkVersions: false }.
100
+ ```
47
101
 
48
- await manager.load({
49
- name: "audit-log",
50
- version: "1.0.0",
51
- activate: async (ctx) => ctx.logger.info("audit-log activated"),
102
+ Supported range forms are an exact version (`1.2.3`), caret and tilde
103
+ (`^1.2.3`, `~1.2.3`), comparators (`>=1.2.3`, `>`, `<=`, `<`) and `*`.
104
+ Pass `{ checkVersions: false }` to skip the check.
105
+
106
+ ## Manager options
107
+
108
+ ```typescript
109
+ new PluginManager({
110
+ checkVersions: true, // enforce declared dependency versions (default)
111
+ hookTimeout: 5_000, // bound each lifecycle hook; 0 = unbounded (default)
112
+ onError: (error, pluginName) => report(error, pluginName), // teardown failures
113
+ allowedCapabilities: ["http", "db"], // reject plugins requesting anything else
114
+ events: eventSink, // receives `plugin:registered`
52
115
  });
53
116
  ```
54
117
 
118
+ ## Events
119
+
120
+ Lifecycle events are emitted on `context.events` if you supply one, and
121
+ `plugin:registered` on the manager's own `events` option. Names are in
122
+ `PLUGIN_EVENTS`; payloads are `PluginLifecycleEvent`.
123
+
124
+ ```typescript
125
+ import { PLUGIN_EVENTS } from "@zudojs/plugins";
126
+
127
+ const manager = new PluginManager({
128
+ events: {
129
+ on() {},
130
+ off() {},
131
+ emit(name, payload) {
132
+ if (name === PLUGIN_EVENTS.REGISTERED) console.log(payload);
133
+ },
134
+ },
135
+ });
136
+ ```
137
+
138
+ A throwing subscriber never fails the phase that emitted the event.
139
+
140
+ ## Diagnostics
141
+
142
+ ```typescript
143
+ const report = manager.diagnostics();
144
+
145
+ report.total; // plugins registered
146
+ report.healthy; // started and not failed
147
+ report.degraded; // registered but not started
148
+ report.unhealthy; // failed at some point
149
+ report.plugins[0].state; // "started" | "failed" | ...
150
+ report.plugins[0].health.details; // failure message, when unhealthy
151
+ ```
152
+
153
+ A plugin that failed and was then disposed during rollback is still
154
+ reported as failed: the current state alone would not show it.
155
+
55
156
  ## License
56
157
 
57
158
  MIT
package/dist/index.d.ts CHANGED
@@ -3,42 +3,63 @@
3
3
  *
4
4
  * Controlled extension system for the Zudojs framework.
5
5
  *
6
- * Provides plugin registration, dependency resolution, lifecycle management,
7
- * and orchestration for Zudojs applications.
6
+ * Provides plugin registration, dependency resolution, lifecycle
7
+ * management, and orchestration for Zudojs applications.
8
+ *
9
+ * Plugins move through `install` -> `initialize` -> `start` on the way up
10
+ * and `stop` -> `dispose` on the way down. The manager runs each phase
11
+ * across every plugin in dependency order, and rolls back everything it
12
+ * brought up if any phase fails.
8
13
  *
9
14
  * @example
10
15
  * ```ts
11
16
  * import { PluginManager, createPluginContext } from "@zudojs/plugins";
12
17
  *
13
- * const manager = new PluginManager();
18
+ * const manager = new PluginManager({ hookTimeout: 5_000 });
14
19
  *
15
20
  * manager.register({
16
- * metadata: { name: "@zudojs/http" },
17
- * dependencies: [{ name: "@zudojs/events" }],
18
- * async install(context) {
19
- * // register services
21
+ * metadata: { name: "@acme/db", version: "1.0.0" },
22
+ * async start(context) {
23
+ * const pool = openPool();
24
+ * context.registerDisposable({ dispose: () => pool.end() });
20
25
  * },
26
+ * });
27
+ *
28
+ * manager.register({
29
+ * metadata: { name: "@acme/api" },
30
+ * dependencies: [{ name: "@acme/db", version: "^1.0.0" }],
21
31
  * async start(context) {
22
- * // begin active work
32
+ * context.logger?.info("api started");
23
33
  * },
24
34
  * });
25
35
  *
26
- * await manager.start(createPluginContext({ metadata: { name: "@zudojs/http" } }));
36
+ * // `createPluginContext` takes the host's own metadata plus the
37
+ * // services plugins are allowed to reach. Each plugin is handed its
38
+ * // own view of it, naming that plugin.
39
+ * const context = createPluginContext({ name: "@acme/host" });
40
+ *
41
+ * await manager.start(context); // @acme/db, then @acme/api
42
+ * await manager.stop(context); // reverse order; disposables released
27
43
  * ```
28
44
  */
29
45
  export { PluginManager } from "./pluginManager/pluginManager.core.js";
46
+ export type { PluginManagerOptions } from "./pluginManager/pluginManager.core.js";
30
47
  export { PluginRegistryImpl } from "./pluginRegistry/pluginRegistry.core.js";
31
48
  export type { PluginRegistry, RegisteredPlugin, } from "./pluginRegistry/pluginRegistry.core.js";
32
49
  export { DependencyResolver, assertResolutionValid, } from "./pluginDependencies/dependencyResolver.core.js";
33
- export type { DependencyResolution } from "./pluginDependencies/dependencyResolver.core.js";
50
+ export type { DependencyResolution, MissingDependency, ResolvablePlugin, } from "./pluginDependencies/dependencyResolver.core.js";
51
+ export { parseVersion, compareVersions, satisfiesVersion, assertDependencyVersions, } from "./pluginDependencies/versionCheck.core.js";
52
+ export type { SemVer } from "./pluginDependencies/versionCheck.core.js";
53
+ export { PluginDependencyVersionError } from "./pluginDependencies/versionCheck.core.js";
34
54
  export { LifecycleController } from "./pluginLifecycle/pluginLifecycle.core.js";
55
+ export type { LifecycleControllerOptions } from "./pluginLifecycle/pluginLifecycle.core.js";
35
56
  export { PLUGIN_EVENTS, createPluginLifecycleEvent, } from "./pluginEvents/pluginEvent.core.js";
36
57
  export type { PluginLifecycleEvent } from "./pluginEvents/pluginEvent.core.js";
37
58
  export { buildDiagnosticReport, createHealthyHealth, createDegradedHealth, createUnhealthyHealth, } from "./pluginDiagnostics/pluginDiagnostic.core.js";
38
59
  export type { PluginHealth, PluginHealthStatus, PluginDiagnostic, PluginDiagnosticReport, } from "./pluginDiagnostics/pluginDiagnostic.core.js";
39
60
  export { isValidTransition, VALID_STATE_TRANSITIONS, } from "./pluginTypes/pluginState.type.js";
40
- export { createPluginContext } from "./pluginIntegration/pluginContext.core.js";
41
- export type { CreatePluginContextOptions } from "./pluginIntegration/pluginContext.core.js";
61
+ export { createPluginContext, createOwnedPluginContext, } from "./pluginIntegration/pluginContext.core.js";
62
+ export type { CreatePluginContextOptions, OwnedPluginContext, } from "./pluginIntegration/pluginContext.core.js";
42
63
  export { PluginError, PluginRegistrationError, PluginAlreadyRegisteredError, PluginNotFoundError, PluginDependencyError, PluginDependencyCycleError, PluginInitializationError, PluginStartError, PluginStopError, PluginDisposeError, PluginTimeoutError, PluginStateError, createPluginError, isPluginError, } from "@zudojs/errors";
43
- export type { PluginState, PluginMetadata, PluginDependency, PluginContext, Plugin, PluginErrorOptions, } from "./pluginTypes/index.js";
64
+ export type { PluginState, PluginMetadata, PluginDependency, PluginContext, PluginContainer, PluginConfig, PluginLogger, PluginEvents, PluginDisposable, Plugin, PluginErrorOptions, } from "./pluginTypes/index.js";
44
65
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -3,36 +3,54 @@
3
3
  *
4
4
  * Controlled extension system for the Zudojs framework.
5
5
  *
6
- * Provides plugin registration, dependency resolution, lifecycle management,
7
- * and orchestration for Zudojs applications.
6
+ * Provides plugin registration, dependency resolution, lifecycle
7
+ * management, and orchestration for Zudojs applications.
8
+ *
9
+ * Plugins move through `install` -> `initialize` -> `start` on the way up
10
+ * and `stop` -> `dispose` on the way down. The manager runs each phase
11
+ * across every plugin in dependency order, and rolls back everything it
12
+ * brought up if any phase fails.
8
13
  *
9
14
  * @example
10
15
  * ```ts
11
16
  * import { PluginManager, createPluginContext } from "@zudojs/plugins";
12
17
  *
13
- * const manager = new PluginManager();
18
+ * const manager = new PluginManager({ hookTimeout: 5_000 });
14
19
  *
15
20
  * manager.register({
16
- * metadata: { name: "@zudojs/http" },
17
- * dependencies: [{ name: "@zudojs/events" }],
18
- * async install(context) {
19
- * // register services
21
+ * metadata: { name: "@acme/db", version: "1.0.0" },
22
+ * async start(context) {
23
+ * const pool = openPool();
24
+ * context.registerDisposable({ dispose: () => pool.end() });
20
25
  * },
26
+ * });
27
+ *
28
+ * manager.register({
29
+ * metadata: { name: "@acme/api" },
30
+ * dependencies: [{ name: "@acme/db", version: "^1.0.0" }],
21
31
  * async start(context) {
22
- * // begin active work
32
+ * context.logger?.info("api started");
23
33
  * },
24
34
  * });
25
35
  *
26
- * await manager.start(createPluginContext({ metadata: { name: "@zudojs/http" } }));
36
+ * // `createPluginContext` takes the host's own metadata plus the
37
+ * // services plugins are allowed to reach. Each plugin is handed its
38
+ * // own view of it, naming that plugin.
39
+ * const context = createPluginContext({ name: "@acme/host" });
40
+ *
41
+ * await manager.start(context); // @acme/db, then @acme/api
42
+ * await manager.stop(context); // reverse order; disposables released
27
43
  * ```
28
44
  */
29
45
  export { PluginManager } from "./pluginManager/pluginManager.core.js";
30
46
  export { PluginRegistryImpl } from "./pluginRegistry/pluginRegistry.core.js";
31
47
  export { DependencyResolver, assertResolutionValid, } from "./pluginDependencies/dependencyResolver.core.js";
48
+ export { parseVersion, compareVersions, satisfiesVersion, assertDependencyVersions, } from "./pluginDependencies/versionCheck.core.js";
49
+ export { PluginDependencyVersionError } from "./pluginDependencies/versionCheck.core.js";
32
50
  export { LifecycleController } from "./pluginLifecycle/pluginLifecycle.core.js";
33
51
  export { PLUGIN_EVENTS, createPluginLifecycleEvent, } from "./pluginEvents/pluginEvent.core.js";
34
52
  export { buildDiagnosticReport, createHealthyHealth, createDegradedHealth, createUnhealthyHealth, } from "./pluginDiagnostics/pluginDiagnostic.core.js";
35
53
  export { isValidTransition, VALID_STATE_TRANSITIONS, } from "./pluginTypes/pluginState.type.js";
36
- export { createPluginContext } from "./pluginIntegration/pluginContext.core.js";
54
+ export { createPluginContext, createOwnedPluginContext, } from "./pluginIntegration/pluginContext.core.js";
37
55
  export { PluginError, PluginRegistrationError, PluginAlreadyRegisteredError, PluginNotFoundError, PluginDependencyError, PluginDependencyCycleError, PluginInitializationError, PluginStartError, PluginStopError, PluginDisposeError, PluginTimeoutError, PluginStateError, createPluginError, isPluginError, } from "@zudojs/errors";
38
56
  //# sourceMappingURL=index.js.map
@@ -1,10 +1,34 @@
1
1
  import type { PluginDependency } from "../pluginTypes/pluginDependency.type.js";
2
+ /**
3
+ * The dependency shape the resolver needs from a plugin.
4
+ */
5
+ export interface ResolvablePlugin {
6
+ readonly dependencies?: readonly PluginDependency[];
7
+ readonly optionalDependencies?: readonly PluginDependency[];
8
+ }
9
+ /**
10
+ * A dependency that could not be satisfied.
11
+ */
12
+ export interface MissingDependency {
13
+ /** The plugin that declared the dependency. */
14
+ readonly plugin: string;
15
+ /** The dependency it needs. */
16
+ readonly dependency: string;
17
+ }
2
18
  /**
3
19
  * Result of dependency resolution.
4
20
  */
5
21
  export interface DependencyResolution {
6
22
  readonly ordered: readonly string[];
23
+ /**
24
+ * Names of required dependencies that are not registered.
25
+ *
26
+ * Retained as plain names for compatibility; {@link missingDetails}
27
+ * also records which plugin declared each one.
28
+ */
7
29
  readonly missing: readonly string[];
30
+ /** Missing dependencies, with the plugin that declared each. */
31
+ readonly missingDetails: readonly MissingDependency[];
8
32
  readonly cycles: readonly string[];
9
33
  }
10
34
  /**
@@ -13,16 +37,33 @@ export interface DependencyResolution {
13
37
  export declare class DependencyResolver {
14
38
  /**
15
39
  * Resolves dependencies for the given plugins.
40
+ *
41
+ * Optional dependencies that are present participate in ordering — a
42
+ * plugin that optionally integrates with a peer must still start after
43
+ * it — while optional dependencies that are absent are ignored rather
44
+ * than reported missing.
45
+ */
46
+ resolve(plugins: Map<string, ResolvablePlugin>): DependencyResolution;
47
+ /**
48
+ * Returns the edges a plugin depends on, in declaration order.
49
+ *
50
+ * Only dependencies that are actually registered become edges, so an
51
+ * absent optional dependency does not create a dangling node.
52
+ */
53
+ private edgesFor;
54
+ /**
55
+ * Produces a dependency-first ordering, recording any cycles found.
56
+ *
57
+ * The traversal keeps its own stack rather than recursing, so a deep
58
+ * dependency chain reports a graph result instead of overflowing.
16
59
  */
17
- resolve(plugins: Map<string, {
18
- readonly dependencies?: readonly PluginDependency[];
19
- readonly optionalDependencies?: readonly PluginDependency[];
20
- }>): DependencyResolution;
21
- private dfs;
22
- private extractCycle;
60
+ private topologicalOrder;
23
61
  }
24
62
  /**
25
63
  * Throws if the dependency resolution has errors.
64
+ *
65
+ * The thrown error names both the plugin that declared the dependency
66
+ * and the dependency itself, so a startup failure says who needed what.
26
67
  */
27
68
  export declare function assertResolutionValid(resolution: DependencyResolution): void;
28
69
  //# sourceMappingURL=dependencyResolver.core.d.ts.map
@@ -5,79 +5,141 @@ import { PluginDependencyCycleError, PluginDependencyError, } from "@zudojs/erro
5
5
  export class DependencyResolver {
6
6
  /**
7
7
  * Resolves dependencies for the given plugins.
8
+ *
9
+ * Optional dependencies that are present participate in ordering — a
10
+ * plugin that optionally integrates with a peer must still start after
11
+ * it — while optional dependencies that are absent are ignored rather
12
+ * than reported missing.
8
13
  */
9
14
  resolve(plugins) {
10
- const missing = [];
15
+ const missingDetails = [];
11
16
  const cycles = [];
12
17
  for (const [name, plugin] of plugins) {
13
18
  for (const dep of plugin.dependencies ?? []) {
14
19
  if (!plugins.has(dep.name)) {
15
- missing.push(dep.name);
20
+ missingDetails.push({ plugin: name, dependency: dep.name });
16
21
  }
17
22
  }
18
23
  }
19
- if (missing.length > 0) {
20
- return { ordered: [], missing, cycles };
21
- }
22
- const visited = new Set();
23
- const visiting = new Set();
24
- const order = [];
25
- for (const name of plugins.keys()) {
26
- if (!visited.has(name)) {
27
- const cycle = this.dfs(name, plugins, visited, visiting, order);
28
- if (cycle) {
29
- cycles.push(cycle);
30
- }
31
- }
24
+ const missing = missingDetails.map((entry) => entry.dependency);
25
+ if (missingDetails.length > 0) {
26
+ return Object.freeze({
27
+ ordered: Object.freeze([]),
28
+ missing: Object.freeze(missing),
29
+ missingDetails: Object.freeze(missingDetails),
30
+ cycles: Object.freeze(cycles),
31
+ });
32
32
  }
33
+ const order = this.topologicalOrder(plugins, cycles);
33
34
  if (cycles.length > 0) {
34
- return { ordered: [], missing, cycles };
35
+ return Object.freeze({
36
+ ordered: Object.freeze([]),
37
+ missing: Object.freeze(missing),
38
+ missingDetails: Object.freeze(missingDetails),
39
+ cycles: Object.freeze(cycles),
40
+ });
35
41
  }
36
- return {
42
+ return Object.freeze({
37
43
  ordered: Object.freeze(order),
38
- missing,
44
+ missing: Object.freeze(missing),
45
+ missingDetails: Object.freeze(missingDetails),
39
46
  cycles: Object.freeze(cycles),
40
- };
47
+ });
41
48
  }
42
- dfs(name, plugins, visited, visiting, order) {
43
- if (visiting.has(name)) {
44
- const cycle = this.extractCycle(name, visiting);
45
- return cycle;
49
+ /**
50
+ * Returns the edges a plugin depends on, in declaration order.
51
+ *
52
+ * Only dependencies that are actually registered become edges, so an
53
+ * absent optional dependency does not create a dangling node.
54
+ */
55
+ edgesFor(name, plugins) {
56
+ const plugin = plugins.get(name);
57
+ if (!plugin) {
58
+ return [];
46
59
  }
47
- if (visited.has(name)) {
48
- return undefined;
60
+ const edges = [];
61
+ for (const dep of plugin.dependencies ?? []) {
62
+ if (plugins.has(dep.name)) {
63
+ edges.push(dep.name);
64
+ }
49
65
  }
50
- visiting.add(name);
51
- const plugin = plugins.get(name);
52
- const deps = plugin?.dependencies ?? [];
53
- for (const dep of deps) {
54
- const cycle = this.dfs(dep.name, plugins, visited, visiting, order);
55
- if (cycle) {
56
- return cycle;
66
+ for (const dep of plugin.optionalDependencies ?? []) {
67
+ if (plugins.has(dep.name) && !edges.includes(dep.name)) {
68
+ edges.push(dep.name);
57
69
  }
58
70
  }
59
- visiting.delete(name);
60
- visited.add(name);
61
- order.push(name);
62
- return undefined;
71
+ return edges;
63
72
  }
64
- extractCycle(entryPoint, visiting) {
65
- const cycle = [entryPoint];
66
- let current = entryPoint;
67
- const entries = Array.from(visiting);
68
- const startIndex = entries.indexOf(current);
69
- for (let i = startIndex + 1; i < entries.length; i++) {
70
- cycle.push(entries[i]);
71
- current = entries[i];
73
+ /**
74
+ * Produces a dependency-first ordering, recording any cycles found.
75
+ *
76
+ * The traversal keeps its own stack rather than recursing, so a deep
77
+ * dependency chain reports a graph result instead of overflowing.
78
+ */
79
+ topologicalOrder(plugins, cycles) {
80
+ const visited = new Set();
81
+ const onStack = new Set();
82
+ const order = [];
83
+ for (const root of plugins.keys()) {
84
+ if (visited.has(root)) {
85
+ continue;
86
+ }
87
+ // Each frame tracks how many of the node's edges have been walked.
88
+ const stack = [
89
+ { name: root, edgeIndex: 0, path: [root] },
90
+ ];
91
+ onStack.add(root);
92
+ while (stack.length > 0) {
93
+ const frame = stack[stack.length - 1];
94
+ const edges = this.edgesFor(frame.name, plugins);
95
+ if (frame.edgeIndex >= edges.length) {
96
+ onStack.delete(frame.name);
97
+ visited.add(frame.name);
98
+ order.push(frame.name);
99
+ stack.pop();
100
+ continue;
101
+ }
102
+ const next = edges[frame.edgeIndex];
103
+ frame.edgeIndex += 1;
104
+ if (onStack.has(next)) {
105
+ const start = frame.path.indexOf(next);
106
+ const cycle = start >= 0 ? frame.path.slice(start) : [next, frame.name];
107
+ cycles.push([...cycle, next].join(" -> "));
108
+ // Unwind: an ordering cannot be produced for a cyclic graph.
109
+ for (const remaining of stack) {
110
+ onStack.delete(remaining.name);
111
+ }
112
+ stack.length = 0;
113
+ break;
114
+ }
115
+ if (visited.has(next)) {
116
+ continue;
117
+ }
118
+ onStack.add(next);
119
+ stack.push({
120
+ name: next,
121
+ edgeIndex: 0,
122
+ path: [...frame.path, next],
123
+ });
124
+ }
125
+ if (cycles.length > 0) {
126
+ break;
127
+ }
72
128
  }
73
- cycle.push(entryPoint);
74
- return cycle.join(" -> ");
129
+ return order;
75
130
  }
76
131
  }
77
132
  /**
78
133
  * Throws if the dependency resolution has errors.
134
+ *
135
+ * The thrown error names both the plugin that declared the dependency
136
+ * and the dependency itself, so a startup failure says who needed what.
79
137
  */
80
138
  export function assertResolutionValid(resolution) {
139
+ const missing = resolution.missingDetails[0];
140
+ if (missing) {
141
+ throw new PluginDependencyError(missing.plugin, missing.dependency);
142
+ }
81
143
  if (resolution.missing.length > 0) {
82
144
  throw new PluginDependencyError(resolution.missing[0], resolution.missing[0]);
83
145
  }
@@ -1,8 +1,12 @@
1
1
  /**
2
2
  * @zudojs/plugins/pluginDependencies
3
3
  *
4
- * Plugin dependency resolution, topological sorting, and cycle detection.
4
+ * Plugin dependency resolution, topological sorting, cycle detection,
5
+ * and version constraint checking.
5
6
  */
6
- export type { DependencyResolution } from "./dependencyResolver.core.js";
7
+ export type { DependencyResolution, MissingDependency, ResolvablePlugin, } from "./dependencyResolver.core.js";
7
8
  export { DependencyResolver, assertResolutionValid, } from "./dependencyResolver.core.js";
9
+ export type { SemVer } from "./versionCheck.core.js";
10
+ export { PluginDependencyVersionError } from "./versionCheck.core.js";
11
+ export { parseVersion, compareVersions, satisfiesVersion, assertDependencyVersions, } from "./versionCheck.core.js";
8
12
  //# sourceMappingURL=index.d.ts.map
@@ -1,7 +1,10 @@
1
1
  /**
2
2
  * @zudojs/plugins/pluginDependencies
3
3
  *
4
- * Plugin dependency resolution, topological sorting, and cycle detection.
4
+ * Plugin dependency resolution, topological sorting, cycle detection,
5
+ * and version constraint checking.
5
6
  */
6
7
  export { DependencyResolver, assertResolutionValid, } from "./dependencyResolver.core.js";
8
+ export { PluginDependencyVersionError } from "./versionCheck.core.js";
9
+ export { parseVersion, compareVersions, satisfiesVersion, assertDependencyVersions, } from "./versionCheck.core.js";
7
10
  //# sourceMappingURL=index.js.map