Skip to content

Plugin manager review - #879

Open
johngeorgewright wants to merge 10 commits into
masterfrom
fix/plugin-manager-review
Open

johngeorgewright wants to merge 10 commits into
masterfrom
fix/plugin-manager-review

Conversation

@johngeorgewright

Copy link
Copy Markdown
Collaborator

No description provided.

johngeorgewright and others added 10 commits September 17, 2026 12:55
`#pluginsToEnable` was a single instance field overwritten on every call to
`enablePlugins`. Because `enablePlugins` is re-entrant (handed to plugins via
the enable context) and runs its children concurrently, nested and concurrent
calls clobbered the field mid-iteration. In particular, enabling a plugin's
hard dependencies replaced the set that optional-dependency resolution reads,
so an optional dependency requested in the same batch was silently not
recognised — and the depender's `enable()` no longer waited for it.

Thread the requested set through `#enablePlugin`/`#enableOptionalDependencies`
as batch-local state, and track in-flight batches in `#pendingBatches` so
`disablePlugins` can still cancel a plugin that is disabled mid-enable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`#addPlugin` resolved each dependency name to its Plugin object eagerly via
`#getPlugin`, which threw when the dependency hadn't been registered yet. That
forced consumers to register plugins in strict dependency order.

Wire a dependency edge immediately when the dependency is already registered,
otherwise record it and wire it up when the dependency is later registered. A
dependency that is never registered is handled as before at enable time (a
warning, and it's skipped).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…gins

`enablePlugins` used to `console.warn` and continue on an unregistered name
while `disablePlugins` threw — inconsistent, and the console logging wasn't
something consumers could control.

Both paths now route through a single `#handleUnknownPlugin` that throws by
default. A new `onUnknownPlugin(pluginName, phase)` option lets consumers log,
collect, or otherwise tolerate unknown plugins; if the handler doesn't throw,
the name is skipped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Previously a plugin that threw from enable()/run() rejected the whole batch
via Promise.all, and a failed enable left the plugin marked as enabled (it was
added to #enabledPlugins optimistically, before enable ran).

Catch errors per plugin so siblings keep going, roll a failed enable back out
of #enabledPlugins (and abort it), and surface the error through a new
onPluginError(error, plugin, phase) option. It defaults to console.error;
throwing from the handler restores fail-fast behaviour.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`#runPlugin` resolves a plugin's dependencies before marking it as run, so a
dependency cycle recursed forever (the enable phase is already guarded by the
`#enabledPlugins` check). Track the dependency path down the recursion and
throw a clear "Circular dependency detected: a -> b -> a" error instead of
hanging.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The enable phase orders optional dependencies, but run() only waited on hard
dependencies, so a present optional dependency could run after its depender.
Fold enabled optional dependencies into the run ordering (skipping ones that
aren't enabled, so they still don't gate the run), sharing the same circular
-dependency detection as hard dependencies.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
withOptions copied the #abortControllers WeakMap into the clone but not
#enabledPlugins/#ran. Because the abort listeners captured the manager that
created each controller, aborting or disabling a plugin through the clone
mutated the original manager's state. Share only the static registration
(#plugins, #dependencyGraph) and let the clone keep its own runtime state.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Remove the unused `DependencyGraph.names()` generator (never called, and
  `DependencyGraph` isn't exported); it was the only in-package caller of the
  misspelled `Graph.verticies()`.
- Default `Plugin`'s context generics to `Record<string, unknown>` instead of
  `any`, so a bare `Plugin` keeps context type-safety. Existing consumers pass
  the type arguments explicitly and are unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
BREAKING CHANGE: `amendKey` and `filter` have been removed in favour of `set`.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant