@zudojs/plugins 0.0.1 → 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.
- package/README.md +130 -29
- package/dist/index.d.ts +34 -13
- package/dist/index.js +28 -10
- package/dist/pluginDependencies/dependencyResolver.core.d.ts +47 -6
- package/dist/pluginDependencies/dependencyResolver.core.js +108 -46
- package/dist/pluginDependencies/index.d.ts +6 -2
- package/dist/pluginDependencies/index.js +4 -1
- package/dist/pluginDependencies/versionCheck.core.d.ts +67 -0
- package/dist/pluginDependencies/versionCheck.core.js +167 -0
- package/dist/pluginDiagnostics/pluginDiagnostic.core.d.ts +9 -0
- package/dist/pluginDiagnostics/pluginDiagnostic.core.js +16 -12
- package/dist/pluginIntegration/index.d.ts +2 -1
- package/dist/pluginIntegration/index.js +1 -1
- package/dist/pluginIntegration/pluginContext.core.d.ts +35 -5
- package/dist/pluginIntegration/pluginContext.core.js +28 -5
- package/dist/pluginLifecycle/pluginLifecycle.core.d.ts +42 -0
- package/dist/pluginLifecycle/pluginLifecycle.core.js +132 -70
- package/dist/pluginManager/pluginManager.core.d.ts +121 -3
- package/dist/pluginManager/pluginManager.core.js +271 -47
- package/dist/pluginRegistry/pluginRegistry.core.d.ts +15 -6
- package/dist/pluginRegistry/pluginRegistry.core.js +6 -1
- package/dist/pluginTypes/index.d.ts +1 -1
- package/dist/pluginTypes/pluginContext.type.d.ts +11 -3
- package/dist/pluginTypes/pluginState.type.js +6 -3
- package/package.json +15 -8
- package/dist/.tsbuildinfo +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/pluginDependencies/dependencyResolver.core.d.ts.map +0 -1
- package/dist/pluginDependencies/dependencyResolver.core.js.map +0 -1
- package/dist/pluginDependencies/index.d.ts.map +0 -1
- package/dist/pluginDependencies/index.js.map +0 -1
- package/dist/pluginDiagnostics/index.d.ts.map +0 -1
- package/dist/pluginDiagnostics/index.js.map +0 -1
- package/dist/pluginDiagnostics/pluginDiagnostic.core.d.ts.map +0 -1
- package/dist/pluginDiagnostics/pluginDiagnostic.core.js.map +0 -1
- package/dist/pluginEvents/index.d.ts.map +0 -1
- package/dist/pluginEvents/index.js.map +0 -1
- package/dist/pluginEvents/pluginEvent.core.d.ts.map +0 -1
- package/dist/pluginEvents/pluginEvent.core.js.map +0 -1
- package/dist/pluginIntegration/index.d.ts.map +0 -1
- package/dist/pluginIntegration/index.js.map +0 -1
- package/dist/pluginIntegration/pluginContext.core.d.ts.map +0 -1
- package/dist/pluginIntegration/pluginContext.core.js.map +0 -1
- package/dist/pluginLifecycle/pluginLifecycle.core.d.ts.map +0 -1
- package/dist/pluginLifecycle/pluginLifecycle.core.js.map +0 -1
- package/dist/pluginManager/pluginManager.core.d.ts.map +0 -1
- package/dist/pluginManager/pluginManager.core.js.map +0 -1
- package/dist/pluginRegistry/index.d.ts.map +0 -1
- package/dist/pluginRegistry/index.js.map +0 -1
- package/dist/pluginRegistry/pluginRegistry.core.d.ts.map +0 -1
- package/dist/pluginRegistry/pluginRegistry.core.js.map +0 -1
- package/dist/pluginTypes/index.d.ts.map +0 -1
- package/dist/pluginTypes/index.js.map +0 -1
- package/dist/pluginTypes/plugin.type.d.ts.map +0 -1
- package/dist/pluginTypes/plugin.type.js.map +0 -1
- package/dist/pluginTypes/pluginContext.type.d.ts.map +0 -1
- package/dist/pluginTypes/pluginContext.type.js.map +0 -1
- package/dist/pluginTypes/pluginDependency.type.d.ts.map +0 -1
- package/dist/pluginTypes/pluginDependency.type.js.map +0 -1
- package/dist/pluginTypes/pluginMetadata.type.d.ts.map +0 -1
- package/dist/pluginTypes/pluginMetadata.type.js.map +0 -1
- package/dist/pluginTypes/pluginState.type.d.ts.map +0 -1
- package/dist/pluginTypes/pluginState.type.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
# @zudojs/plugins
|
|
2
2
|
|
|
3
|
-
Plugin manager, registry, dependency resolver, lifecycle controller, events, and
|
|
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
|
-
-
|
|
10
|
-
-
|
|
11
|
-
- run plugin lifecycle hooks (install →
|
|
12
|
-
-
|
|
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
|
-
##
|
|
21
|
+
## Quick start
|
|
21
22
|
|
|
22
23
|
```typescript
|
|
23
|
-
import {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
46
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
|
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: "@
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
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
|
-
*
|
|
32
|
+
* context.logger?.info("api started");
|
|
23
33
|
* },
|
|
24
34
|
* });
|
|
25
35
|
*
|
|
26
|
-
*
|
|
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
|
|
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: "@
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
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
|
-
*
|
|
32
|
+
* context.logger?.info("api started");
|
|
23
33
|
* },
|
|
24
34
|
* });
|
|
25
35
|
*
|
|
26
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
20
|
+
missingDetails.push({ plugin: name, dependency: dep.name });
|
|
16
21
|
}
|
|
17
22
|
}
|
|
18
23
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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 {
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
48
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
60
|
-
visited.add(name);
|
|
61
|
-
order.push(name);
|
|
62
|
-
return undefined;
|
|
71
|
+
return edges;
|
|
63
72
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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
|