Skip to content

obsidian/components/plugin-gate-component

The gate on a plugin’s feature surface: what must be PRESENT for it to run, and what must NOT be.

Both halves answer the same question — may this plugin’s onloadImpl run right now? — so they are one component rather than two. A second, independent gate would double-load the surface and stack a second “blocked” settings tab onto the same plugin, and neither could see the other’s reason for refusing.

Dependencies: a plugin declaring what it cannot work without

Section titled “Dependencies: a plugin declaring what it cannot work without”

This is the strict sibling of PluginSuggestionComponent, and the difference is the whole point. A SUGGESTION is an offer — the host keeps working without it and simply cannot do one thing. A DEPENDENCY is a requirement: onloadImpl never runs, no command is registered, no handler is installed, nothing is patched. Reach for a suggestion when the other plugin adds something; reach for a dependency when the host’s advertised behavior is not there without it.

Obsidian has no dependency field in a manifest, which is exactly the problem this closes. Without one, a user meeting an unfamiliar plugin in their list months later has nothing telling them it is load-bearing — they remove it, and the breakage surfaces weeks after that as damage they cannot connect to anything they did. So the relationship is made visible from both ends and enforced while it holds: the dependent explains itself where the user is looking, repairs itself in one click, and reacts the INSTANT the dependency goes away rather than degrading quietly.

Three choices are deliberate and worth stating, because each has an obvious-looking alternative:

  • Blocked means enabled-but-inert, never self-disabled. Calling disablePluginAndSave on itself would be the literal reading of “refuses to load”, and it rewrites a config that belongs to the user — who would then have to remember to re-enable it after fixing the dependency. Staying enabled and inert says the same thing to the user, changes nothing they own, and can undo itself. - Blocked is not an error. Throwing from onload marks the plugin failed in Obsidian and hands the user a stack trace instead of a sentence and a button. - A dependency must publish an API. That is what makes presence, absence, version and departure all observable through one mechanism — watchPluginApi’s reference is live, is revoked when the provider unloads, and already classifies not-installed against not-enabled against too-old. A provider with nothing to expose can publish an empty API purely so it can be depended upon; the alternative would be a second, weaker detection path to maintain beside it.

Conflicts: a plugin declaring what it refuses to run beside

Section titled “Conflicts: a plugin declaring what it refuses to run beside”

The inverse relationship, and it needs its own detection path because the dependency one does not fit. A conflicting plugin is typically an OLD version of some other plugin that still owns behavior this one has taken over — it publishes no API at all, so there is no apiVersionRange to compare against.

The argument for refusing rather than competing comes from the case this generalizes: two rename/delete handlers acting on one rename corrupt links and move attachments twice, and there is no reliable way to win that race — a handler is elected by registry order, but the patches that do the work sit outside that election, so whichever plugin loaded first keeps a hand on the wheel. Every scheme for seizing control from inside is load-order dependent. Refusing is deterministic where competing is not, and a vault that briefly has no handler is a far better outcome than one with two.

Two details of that detection carry over verbatim, because both are load-bearing:

  • Versions are read, never the registry. A plugin that has not loaded yet has registered nothing, so asking the registry gives a different answer depending on when it is asked. app.plugins.manifests is populated for every installed plugin at startup, whatever the load order turns out to be. - An unparseable version counts as conflicting. Failing closed is the safe direction: a false alarm costs a notice, a false all-clear costs a vault.

Not every overlap is that severe, which is why a conflict declares a PluginConflictSeverity. Two plugins shipping the same command duplicate a palette entry and do the work twice — annoying, not corrupting — and refusing to load over that would be a worse outcome than the overlap. Those declare PluginConflictSeverity.Warn and both keep running.

Obsidian raises no event when ANOTHER plugin is enabled or disabled (see plugin-lifecycle-events.ts), so live conflict detection has a real ceiling and it is stated here rather than implied away. A conflicting plugin built on this library announces itself through the lifecycle broadcast, and is therefore caught the moment it is enabled or disabled. One that is not — an old release predating the broadcast, or a plugin by another author entirely — is caught at the next load, which is when its manifest is read.

ClassDescription
PluginGateComponentEnforces a plugin’s mandatory dependencies and declared conflicts, gating its feature surface on both.
InterfaceDescription
PluginConflictA plugin another plugin refuses to run beside, or warns about running beside.
PluginDependencyA plugin another plugin cannot work without.
PluginGateComponentConstructorParamsParameters for the PluginGateComponent constructor.
EnumDescription
PluginConflictSeverityHow badly two plugins running side by side goes wrong, and therefore what this one does about it.