@zudojs/plugins 0.1.0 → 1.1.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/LICENSE +21 -0
- package/README.md +135 -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 +199 -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 +282 -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 +26 -15
- 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
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
import { PluginRegistryImpl } from "../pluginRegistry/pluginRegistry.core.js";
|
|
2
2
|
import { DependencyResolver, assertResolutionValid, } from "../pluginDependencies/dependencyResolver.core.js";
|
|
3
|
+
import { assertDependencyVersions } from "../pluginDependencies/versionCheck.core.js";
|
|
3
4
|
import { LifecycleController } from "../pluginLifecycle/pluginLifecycle.core.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { buildDiagnosticReport
|
|
5
|
+
import { PluginError, PluginRegistrationError, PluginStateError, } from "@zudojs/errors";
|
|
6
|
+
import { createOwnedPluginContext } from "../pluginIntegration/pluginContext.core.js";
|
|
7
|
+
import { buildDiagnosticReport } from "../pluginDiagnostics/pluginDiagnostic.core.js";
|
|
8
|
+
import { PLUGIN_EVENTS, createPluginLifecycleEvent, } from "../pluginEvents/pluginEvent.core.js";
|
|
9
|
+
/**
|
|
10
|
+
* States from which a plugin still owns resources that must be released.
|
|
11
|
+
*/
|
|
12
|
+
const DISPOSABLE_STATES = new Set([
|
|
13
|
+
"installed",
|
|
14
|
+
"initialized",
|
|
15
|
+
"stopped",
|
|
16
|
+
"failed",
|
|
17
|
+
]);
|
|
7
18
|
/**
|
|
8
19
|
* Plugin manager coordinates registration, dependency resolution, lifecycle, and disposal.
|
|
9
20
|
*/
|
|
@@ -12,19 +23,78 @@ export class PluginManager {
|
|
|
12
23
|
resolver;
|
|
13
24
|
lifecycle;
|
|
14
25
|
plugins = new Map();
|
|
15
|
-
|
|
26
|
+
options;
|
|
27
|
+
/**
|
|
28
|
+
* Dependency-first order from the last successful resolution.
|
|
29
|
+
*
|
|
30
|
+
* Shutdown reverses this rather than registration order: tearing down
|
|
31
|
+
* in registration order destroys a dependency while its dependents are
|
|
32
|
+
* still using it.
|
|
33
|
+
*/
|
|
34
|
+
startupOrder = [];
|
|
35
|
+
starting = false;
|
|
36
|
+
/**
|
|
37
|
+
* Per-plugin context views and their abort handles.
|
|
38
|
+
*
|
|
39
|
+
* Each plugin gets a context whose `onDispose` and
|
|
40
|
+
* `registerDisposable` write to that plugin's own disposables list —
|
|
41
|
+
* the list the lifecycle actually drains — and whose `signal` this
|
|
42
|
+
* manager aborts on shutdown.
|
|
43
|
+
*/
|
|
44
|
+
contexts = new Map();
|
|
45
|
+
constructor(options = {}) {
|
|
16
46
|
this.registry = new PluginRegistryImpl();
|
|
17
47
|
this.resolver = new DependencyResolver();
|
|
18
|
-
this.lifecycle = new LifecycleController(
|
|
48
|
+
this.lifecycle = new LifecycleController(options.hookTimeout !== undefined
|
|
49
|
+
? { hookTimeout: options.hookTimeout }
|
|
50
|
+
: {});
|
|
51
|
+
this.options = options;
|
|
19
52
|
}
|
|
20
|
-
|
|
53
|
+
/**
|
|
54
|
+
* Registers a plugin.
|
|
55
|
+
*
|
|
56
|
+
* @throws {PluginAlreadyRegisteredError} when the name is taken, so
|
|
57
|
+
* callers can implement idempotent registration.
|
|
58
|
+
*/
|
|
59
|
+
register(plugin, options) {
|
|
60
|
+
if (typeof plugin?.metadata?.name !== "string" ||
|
|
61
|
+
plugin.metadata.name.length === 0) {
|
|
62
|
+
throw new PluginRegistrationError("Plugin metadata must include a non-empty name.");
|
|
63
|
+
}
|
|
64
|
+
const allowed = this.options.allowedCapabilities;
|
|
65
|
+
if (allowed) {
|
|
66
|
+
const requested = plugin.metadata.capabilities ?? [];
|
|
67
|
+
const denied = requested.filter((capability) => !allowed.includes(capability));
|
|
68
|
+
if (denied.length > 0) {
|
|
69
|
+
throw new PluginRegistrationError(`Plugin "${plugin.metadata.name}" requests capabilities that are not granted: ${denied.join(", ")}.`, plugin.metadata.name);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
21
72
|
try {
|
|
22
|
-
this.registry.register(plugin);
|
|
73
|
+
this.registry.register(plugin, options);
|
|
23
74
|
this.plugins.set(plugin.metadata.name, plugin);
|
|
75
|
+
this.emitRegistered(plugin);
|
|
24
76
|
}
|
|
25
77
|
catch (error) {
|
|
26
|
-
|
|
78
|
+
// Typed plugin errors already say what went wrong and let callers
|
|
79
|
+
// branch on the cause; only unexpected failures are wrapped.
|
|
80
|
+
if (error instanceof PluginError) {
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
throw new PluginRegistrationError(error instanceof Error ? error.message : String(error), plugin.metadata.name);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Removes a plugin that has not been started.
|
|
88
|
+
*
|
|
89
|
+
* Keeps the manager's own view in step with the registry; leaving a
|
|
90
|
+
* removed plugin in `plugins` would keep it in dependency resolution.
|
|
91
|
+
*/
|
|
92
|
+
unregister(name) {
|
|
93
|
+
const removed = this.registry.remove(name);
|
|
94
|
+
if (removed) {
|
|
95
|
+
this.plugins.delete(name);
|
|
27
96
|
}
|
|
97
|
+
return removed;
|
|
28
98
|
}
|
|
29
99
|
get(name) {
|
|
30
100
|
return this.registry.get(name)?.plugin;
|
|
@@ -35,66 +105,231 @@ export class PluginManager {
|
|
|
35
105
|
list() {
|
|
36
106
|
return this.registry.list().map((r) => r.plugin);
|
|
37
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* Installs, initializes and starts every plugin in dependency order.
|
|
110
|
+
*
|
|
111
|
+
* A failure anywhere rolls back: everything already brought up is
|
|
112
|
+
* stopped and disposed before the error is rethrown, so an aborted
|
|
113
|
+
* startup does not strand resources with no route to release them.
|
|
114
|
+
*/
|
|
38
115
|
async start(context) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
116
|
+
if (this.starting) {
|
|
117
|
+
throw new PluginRegistrationError("Plugin manager start is already in progress.");
|
|
118
|
+
}
|
|
119
|
+
this.starting = true;
|
|
120
|
+
try {
|
|
121
|
+
const resolution = this.resolver.resolve(this.toDependencyMap());
|
|
122
|
+
assertResolutionValid(resolution);
|
|
123
|
+
if (this.options.checkVersions ?? true) {
|
|
124
|
+
assertDependencyVersions(this.plugins);
|
|
125
|
+
}
|
|
126
|
+
this.startupOrder = resolution.ordered;
|
|
127
|
+
// A disposed plugin has released its resources and the state
|
|
128
|
+
// machine offers no way back. `start()` used to skip such plugins
|
|
129
|
+
// silently — so a second `start()` after `stop()` resolved with
|
|
130
|
+
// nothing running, and after a rolled-back startup it brought up
|
|
131
|
+
// still-registered dependents on top of disposed dependencies.
|
|
132
|
+
for (const name of this.startupOrder) {
|
|
133
|
+
const registered = this.registry.get(name);
|
|
134
|
+
if (registered?.state === "disposed") {
|
|
135
|
+
throw new PluginStateError(name, "disposed", "installing");
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
try {
|
|
139
|
+
for (const name of this.startupOrder) {
|
|
140
|
+
const registered = this.registry.get(name);
|
|
141
|
+
if (!registered)
|
|
142
|
+
continue;
|
|
143
|
+
// Guarded so a restart — where plugins are already installed
|
|
144
|
+
// and merely stopped — re-runs only the phases it needs.
|
|
145
|
+
if (registered.state === "registered") {
|
|
146
|
+
await this.lifecycle.install(registered, this.contextFor(registered, context));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
for (const name of this.startupOrder) {
|
|
150
|
+
const registered = this.registry.get(name);
|
|
151
|
+
if (!registered)
|
|
152
|
+
continue;
|
|
153
|
+
if (registered.state === "installed") {
|
|
154
|
+
await this.lifecycle.initialize(registered, this.contextFor(registered, context));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
for (const name of this.startupOrder) {
|
|
158
|
+
const registered = this.registry.get(name);
|
|
159
|
+
if (!registered)
|
|
160
|
+
continue;
|
|
161
|
+
if (registered.state === "initialized" ||
|
|
162
|
+
registered.state === "stopped") {
|
|
163
|
+
await this.lifecycle.start(registered, this.contextFor(registered, context));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
await this.rollback(context);
|
|
169
|
+
throw error;
|
|
46
170
|
}
|
|
47
|
-
await this.lifecycle.install(registered, context);
|
|
48
171
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
172
|
+
finally {
|
|
173
|
+
this.starting = false;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Stops and disposes every plugin, in reverse dependency order.
|
|
178
|
+
*
|
|
179
|
+
* Teardown continues past a failing plugin so one bad `stop` cannot
|
|
180
|
+
* strand the rest, and every failure is reported through `onError`
|
|
181
|
+
* rather than swallowed.
|
|
182
|
+
*/
|
|
183
|
+
async stop(context) {
|
|
184
|
+
for (const registered of this.teardownOrder()) {
|
|
185
|
+
if (registered.state !== "started")
|
|
52
186
|
continue;
|
|
187
|
+
try {
|
|
188
|
+
await this.lifecycle.stop(registered, this.contextFor(registered, context));
|
|
53
189
|
}
|
|
54
|
-
|
|
55
|
-
|
|
190
|
+
catch (error) {
|
|
191
|
+
this.report(error, registered.plugin.metadata.name);
|
|
56
192
|
}
|
|
57
193
|
}
|
|
58
|
-
for (const
|
|
59
|
-
|
|
60
|
-
|
|
194
|
+
for (const registered of this.teardownOrder()) {
|
|
195
|
+
// Anything that still holds resources is disposed, including
|
|
196
|
+
// plugins that were installed or initialized but never started
|
|
197
|
+
// because an earlier plugin failed.
|
|
198
|
+
if (!DISPOSABLE_STATES.has(registered.state))
|
|
61
199
|
continue;
|
|
200
|
+
const name = registered.plugin.metadata.name;
|
|
201
|
+
try {
|
|
202
|
+
// Abort first so work the plugin started sees the shutdown
|
|
203
|
+
// before its disposables are released underneath it.
|
|
204
|
+
this.abortContext(name);
|
|
205
|
+
await this.lifecycle.dispose(registered, this.contextFor(registered, context));
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
this.report(error, name);
|
|
62
209
|
}
|
|
63
|
-
|
|
64
|
-
|
|
210
|
+
finally {
|
|
211
|
+
this.contexts.delete(name);
|
|
65
212
|
}
|
|
66
213
|
}
|
|
67
214
|
}
|
|
68
|
-
|
|
69
|
-
const plugins = this.registry.list()
|
|
70
|
-
|
|
71
|
-
|
|
215
|
+
diagnostics() {
|
|
216
|
+
const plugins = this.registry.list().map((registered) => ({
|
|
217
|
+
plugin: registered.plugin,
|
|
218
|
+
state: registered.state,
|
|
219
|
+
failed: registered.failed,
|
|
220
|
+
error: registered.error,
|
|
221
|
+
}));
|
|
222
|
+
return buildDiagnosticReport(plugins);
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Tears down everything brought up by a failed startup.
|
|
226
|
+
*/
|
|
227
|
+
async rollback(context) {
|
|
228
|
+
for (const registered of this.teardownOrder()) {
|
|
72
229
|
if (registered.state === "started") {
|
|
73
230
|
try {
|
|
74
|
-
await this.lifecycle.stop(registered, context);
|
|
231
|
+
await this.lifecycle.stop(registered, this.contextFor(registered, context));
|
|
75
232
|
}
|
|
76
|
-
catch {
|
|
77
|
-
|
|
233
|
+
catch (error) {
|
|
234
|
+
this.report(error, registered.plugin.metadata.name);
|
|
78
235
|
}
|
|
79
236
|
}
|
|
80
237
|
}
|
|
81
|
-
for (const registered of
|
|
82
|
-
if (
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
238
|
+
for (const registered of this.teardownOrder()) {
|
|
239
|
+
if (!DISPOSABLE_STATES.has(registered.state))
|
|
240
|
+
continue;
|
|
241
|
+
const name = registered.plugin.metadata.name;
|
|
242
|
+
try {
|
|
243
|
+
this.abortContext(name);
|
|
244
|
+
await this.lifecycle.dispose(registered, this.contextFor(registered, context));
|
|
245
|
+
}
|
|
246
|
+
catch (error) {
|
|
247
|
+
this.report(error, name);
|
|
248
|
+
}
|
|
249
|
+
finally {
|
|
250
|
+
this.contexts.delete(name);
|
|
89
251
|
}
|
|
90
252
|
}
|
|
91
253
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
254
|
+
/**
|
|
255
|
+
* Registered plugins in reverse dependency order.
|
|
256
|
+
*
|
|
257
|
+
* Falls back to reverse registration order for plugins registered
|
|
258
|
+
* after the last resolution, which is the best available guess.
|
|
259
|
+
*/
|
|
260
|
+
teardownOrder() {
|
|
261
|
+
const seen = new Set();
|
|
262
|
+
const ordered = [];
|
|
263
|
+
for (let i = this.startupOrder.length - 1; i >= 0; i -= 1) {
|
|
264
|
+
const name = this.startupOrder[i];
|
|
265
|
+
const registered = this.registry.get(name);
|
|
266
|
+
if (registered) {
|
|
267
|
+
seen.add(name);
|
|
268
|
+
ordered.push(registered);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
const remaining = this.registry
|
|
272
|
+
.list()
|
|
273
|
+
.filter((registered) => !seen.has(registered.plugin.metadata.name))
|
|
274
|
+
.reverse();
|
|
275
|
+
return [...ordered, ...remaining];
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Returns the plugin's own context view, creating it on first use.
|
|
279
|
+
*
|
|
280
|
+
* The base context supplies the shared services; only the identity,
|
|
281
|
+
* the disposables collection and the abort signal are per-plugin.
|
|
282
|
+
*/
|
|
283
|
+
contextFor(registered, base) {
|
|
284
|
+
const name = registered.plugin.metadata.name;
|
|
285
|
+
const existing = this.contexts.get(name);
|
|
286
|
+
if (existing) {
|
|
287
|
+
return existing.context;
|
|
288
|
+
}
|
|
289
|
+
const owned = createOwnedPluginContext(registered.plugin.metadata, {
|
|
290
|
+
container: base.container,
|
|
291
|
+
config: base.config,
|
|
292
|
+
logger: base.logger,
|
|
293
|
+
events: base.events,
|
|
294
|
+
disposables: registered.disposables,
|
|
295
|
+
});
|
|
296
|
+
this.contexts.set(name, { context: owned.context, abort: owned.abort });
|
|
297
|
+
return owned.context;
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Aborts a plugin's signal, keeping the context available.
|
|
301
|
+
*
|
|
302
|
+
* The entry stays so `dispose` still receives the same — now aborted —
|
|
303
|
+
* context; removing it here would hand the dispose hook a fresh
|
|
304
|
+
* context whose signal had never fired.
|
|
305
|
+
*/
|
|
306
|
+
abortContext(name, reason) {
|
|
307
|
+
this.contexts.get(name)?.abort(reason);
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Emits `plugin:registered`, containing a throwing subscriber so a bad
|
|
311
|
+
* listener cannot fail the registration that triggered it.
|
|
312
|
+
*/
|
|
313
|
+
emitRegistered(plugin) {
|
|
314
|
+
const events = this.options.events;
|
|
315
|
+
if (!events) {
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
try {
|
|
319
|
+
events.emit(PLUGIN_EVENTS.REGISTERED, createPluginLifecycleEvent(plugin.metadata, "registered"));
|
|
320
|
+
}
|
|
321
|
+
catch (error) {
|
|
322
|
+
this.report(error, plugin.metadata.name);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
report(error, pluginName) {
|
|
326
|
+
if (this.options.onError) {
|
|
327
|
+
this.options.onError(error, pluginName);
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
queueMicrotask(() => {
|
|
331
|
+
console.error(`[@zudojs/plugins] Plugin "${pluginName}" failed during teardown.`, error);
|
|
332
|
+
});
|
|
98
333
|
}
|
|
99
334
|
toDependencyMap() {
|
|
100
335
|
const map = new Map();
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Plugin } from "../pluginTypes/plugin.type.js";
|
|
2
2
|
import type { PluginState } from "../pluginTypes/pluginState.type.js";
|
|
3
|
+
import type { PluginDisposable } from "../pluginTypes/pluginContext.type.js";
|
|
3
4
|
/**
|
|
4
5
|
* Internal representation of a registered plugin.
|
|
5
6
|
*/
|
|
@@ -7,13 +8,21 @@ export interface RegisteredPlugin<TPlugin extends Plugin = Plugin> {
|
|
|
7
8
|
readonly plugin: TPlugin;
|
|
8
9
|
state: PluginState;
|
|
9
10
|
readonly options?: unknown;
|
|
10
|
-
readonly disposables:
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
readonly disposables: PluginDisposable[];
|
|
12
|
+
/**
|
|
13
|
+
* Whether this plugin failed at any point in its lifecycle.
|
|
14
|
+
*
|
|
15
|
+
* Tracked separately from `state` because a failed plugin is disposed
|
|
16
|
+
* during rollback, which would otherwise erase every trace of the
|
|
17
|
+
* failure from diagnostics.
|
|
18
|
+
*/
|
|
19
|
+
failed: boolean;
|
|
20
|
+
/** The error that caused the failure, if any. */
|
|
21
|
+
error?: unknown;
|
|
22
|
+
registerDisposable(disposable: PluginDisposable): void;
|
|
16
23
|
setState(state: PluginState): void;
|
|
24
|
+
/** Records a lifecycle failure. */
|
|
25
|
+
setError(error: unknown): void;
|
|
17
26
|
}
|
|
18
27
|
/**
|
|
19
28
|
* Plugin registry interface.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { PluginAlreadyRegisteredError,
|
|
1
|
+
import { PluginAlreadyRegisteredError, PluginStateError } from "@zudojs/errors";
|
|
2
2
|
/**
|
|
3
3
|
* Default plugin registry implementation.
|
|
4
4
|
*/
|
|
@@ -15,12 +15,17 @@ export class PluginRegistryImpl {
|
|
|
15
15
|
state: "registered",
|
|
16
16
|
options,
|
|
17
17
|
disposables,
|
|
18
|
+
failed: false,
|
|
18
19
|
registerDisposable(disposable) {
|
|
19
20
|
disposables.push(disposable);
|
|
20
21
|
},
|
|
21
22
|
setState(state) {
|
|
22
23
|
registered.state = state;
|
|
23
24
|
},
|
|
25
|
+
setError(error) {
|
|
26
|
+
registered.failed = true;
|
|
27
|
+
registered.error = error;
|
|
28
|
+
},
|
|
24
29
|
};
|
|
25
30
|
this.plugins.set(name, registered);
|
|
26
31
|
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
export type { PluginState } from "./pluginState.type.js";
|
|
7
7
|
export type { PluginMetadata } from "./pluginMetadata.type.js";
|
|
8
8
|
export type { PluginDependency } from "./pluginDependency.type.js";
|
|
9
|
-
export type { PluginContext, PluginContainer, PluginConfig, PluginLogger, PluginEvents, } from "./pluginContext.type.js";
|
|
9
|
+
export type { PluginContext, PluginContainer, PluginConfig, PluginLogger, PluginEvents, PluginDisposable, } from "./pluginContext.type.js";
|
|
10
10
|
export type { Plugin } from "./plugin.type.js";
|
|
11
11
|
export type { PluginErrorOptions } from "@zudojs/errors";
|
|
12
12
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -27,6 +27,12 @@ export interface PluginEvents {
|
|
|
27
27
|
off(event: string, handler: (event: unknown) => void): void;
|
|
28
28
|
emit(event: string, payload: unknown): void;
|
|
29
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* Something a plugin registers so the manager releases it on shutdown.
|
|
32
|
+
*/
|
|
33
|
+
export interface PluginDisposable {
|
|
34
|
+
dispose(): void | Promise<void>;
|
|
35
|
+
}
|
|
30
36
|
/**
|
|
31
37
|
* Controlled context provided to plugins during lifecycle operations.
|
|
32
38
|
*/
|
|
@@ -36,10 +42,12 @@ export interface PluginContext {
|
|
|
36
42
|
readonly config?: PluginConfig;
|
|
37
43
|
readonly logger?: PluginLogger;
|
|
38
44
|
readonly events?: PluginEvents;
|
|
45
|
+
/**
|
|
46
|
+
* Aborted when the plugin system shuts down, so long-lived work
|
|
47
|
+
* started by the plugin can stop.
|
|
48
|
+
*/
|
|
39
49
|
readonly signal: AbortSignal;
|
|
40
50
|
onDispose(handler: () => void | Promise<void>): void;
|
|
41
|
-
registerDisposable(disposable:
|
|
42
|
-
dispose(): void | Promise<void>;
|
|
43
|
-
}): void;
|
|
51
|
+
registerDisposable(disposable: PluginDisposable): void;
|
|
44
52
|
}
|
|
45
53
|
//# sourceMappingURL=pluginContext.type.d.ts.map
|
|
@@ -2,11 +2,14 @@
|
|
|
2
2
|
* Valid state transitions for the plugin state machine.
|
|
3
3
|
*/
|
|
4
4
|
export const VALID_STATE_TRANSITIONS = {
|
|
5
|
-
registered: ["installing"],
|
|
5
|
+
registered: ["installing", "disposing"],
|
|
6
6
|
installing: ["installed", "failed"],
|
|
7
|
-
installed
|
|
7
|
+
// A plugin that installed but never started still holds resources, so
|
|
8
|
+
// it must have a route to disposal; without one an aborted startup
|
|
9
|
+
// stranded it permanently.
|
|
10
|
+
installed: ["initializing", "stopping", "disposing", "failed"],
|
|
8
11
|
initializing: ["initialized", "failed"],
|
|
9
|
-
initialized: ["starting", "stopping"],
|
|
12
|
+
initialized: ["starting", "stopping", "disposing", "failed"],
|
|
10
13
|
starting: ["started", "failed"],
|
|
11
14
|
started: ["stopping"],
|
|
12
15
|
stopping: ["stopped", "failed"],
|
package/package.json
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zudojs/plugins",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Plugin system for extending Zudojs applications with modular capabilities.",
|
|
5
5
|
"license": "MIT",
|
|
6
|
+
"author": {
|
|
7
|
+
"name": "Oluwayemi Oyinlola",
|
|
8
|
+
"url": "https://github.com/oyinlola-tech"
|
|
9
|
+
},
|
|
6
10
|
"type": "module",
|
|
7
11
|
"main": "./dist/index.js",
|
|
8
12
|
"module": "./dist/index.js",
|
|
@@ -14,23 +18,19 @@
|
|
|
14
18
|
}
|
|
15
19
|
},
|
|
16
20
|
"files": [
|
|
17
|
-
"dist"
|
|
21
|
+
"dist",
|
|
22
|
+
"!dist/**/*.map",
|
|
23
|
+
"!dist/**/*.tsbuildinfo",
|
|
24
|
+
"!dist/.tsbuildinfo"
|
|
18
25
|
],
|
|
19
|
-
"scripts": {
|
|
20
|
-
"build": "tsc -p tsconfig.json",
|
|
21
|
-
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
22
|
-
"clean": "rm -rf dist",
|
|
23
|
-
"test": "vitest run",
|
|
24
|
-
"test:watch": "vitest"
|
|
25
|
-
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@zudojs/errors": "0.1
|
|
28
|
-
"@zudojs/constants": "0.1
|
|
29
|
-
"@zudojs/types": "
|
|
27
|
+
"@zudojs/errors": "1.0.1",
|
|
28
|
+
"@zudojs/constants": "1.0.1",
|
|
29
|
+
"@zudojs/types": "1.0.0"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@types/node": "^26.4.1",
|
|
33
|
-
"typescript": "
|
|
33
|
+
"typescript": "7.0.2",
|
|
34
34
|
"vitest": "^4.1.11"
|
|
35
35
|
},
|
|
36
36
|
"engines": {
|
|
@@ -45,8 +45,19 @@
|
|
|
45
45
|
"extensions"
|
|
46
46
|
],
|
|
47
47
|
"homepage": "https://github.com/oyinlola-tech/zudo#readme",
|
|
48
|
+
"bugs": {
|
|
49
|
+
"url": "https://github.com/oyinlola-tech/zudo/issues"
|
|
50
|
+
},
|
|
48
51
|
"repository": {
|
|
49
52
|
"type": "git",
|
|
50
|
-
"url": "https://github.com/oyinlola-tech/zudo"
|
|
53
|
+
"url": "https://github.com/oyinlola-tech/zudo",
|
|
54
|
+
"directory": "packages/plugins"
|
|
55
|
+
},
|
|
56
|
+
"scripts": {
|
|
57
|
+
"build": "tsc -p tsconfig.json",
|
|
58
|
+
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.test.json --noEmit",
|
|
59
|
+
"clean": "rm -rf dist",
|
|
60
|
+
"test": "vitest run",
|
|
61
|
+
"test:watch": "vitest"
|
|
51
62
|
}
|
|
52
|
-
}
|
|
63
|
+
}
|