Skip to content

feat: add plugin dependencies - #855

Open
WkdXeqtr wants to merge 8 commits into
SteamClientHomebrew:nextfrom
WkdXeqtr:feat/plugin-deps
Open

feat: add plugin dependencies#855
WkdXeqtr wants to merge 8 commits into
SteamClientHomebrew:nextfrom
WkdXeqtr:feat/plugin-deps

Conversation

@WkdXeqtr

@WkdXeqtr WkdXeqtr commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Closes #823 and #102.

Adds an optional "dependencies" field to plugin.json (and skin.json for themes):

"dependencies": ["some-plugin", "other-plugin@>=1.2.0"]
  • plugins load dependency-first. Backend spawn is sequential and waits for each on_load, so the order is a real guarantee. Cycles warn and fall back to scan order
  • installing a plugin or theme with unmet dependencies shows a warning listing each one with its state. The user can always continue, nothing is ever installed automatically
  • plugin rows in settings get a small chip per dependency with a check or cross

Installing a plugin with unmet dependencies

Same dialog for a theme

Dependency chips in the plugins list

Without the field nothing changes (covered by a test). Version ranges support >=, <=, >, <, =.

This also makes sharing between plugins practical: a plugin can expose objects or CSS variables (shared panel colors and such) and a dependent can actually rely on them, since its dependency is guaranteed to be present and loaded first. Same groundwork the locale system from #823 would build on.

Would like your take on one thing: this is deliberately soft - a missing dependency never blocks installing or enabling, authors are expected to degrade gracefully. If you'd rather also have a hard variant (can't enable without it, npm dependencies vs peerDependencies style), that can be added on top - happy to rework in either direction.

Not here: ordered frontend script execution and a ready signal for plugin frontends, can follow up. Note the install dialogs read pluginJson / skin_data from the site API, which forwards the manifests as-is today.

@ejalxndr

ejalxndr commented Aug 2, 2026

Copy link
Copy Markdown
Member

Big fan of the incremental implementation of this FR. It makes it very easy for me to review.

A hard dependency variant sounds viable.

Major

  1. The dependency-warning log only ever fires once, total, for the entire Steam session (not once per problem as it should).

Minor

  1. we can dedup the O(n) name lookup in the warning loop instead of reusing index_by_name from plugin_deps.cc
  2. DependencyVersionLabel collapses > and >= into identical UI text ("min version X"), which loses distinction that semver::satisfies actually enforces.
  3. plugin_deps.h uses size_t without #include <cstddef>. works rn via transitive includes, but not guaranteed.
  4. Dep chips show config-enabled, not actual running state (PluginComponent.tsx:597). A crashed backend still counts as "enabled" in config, so a dependency chip can show "✓" while the actual guarantee the PR advertises (loaded first) doesn't hold.

Adds an optional "dependencies" array to plugin.json ("name" or
"name@<range>"). Backends now spawn dependency-first (stable topological
sort; spawn is sequential and waits for each plugin's on_load, so the
ordering is real), and frontend shims are injected in the same order.
Cycles, unknown names and unsatisfied version ranges warn once and never
prevent a plugin from loading. Without the field the load order is
unchanged, covered by a Catch2 test. Enumeration order everywhere else
(settings UI, quick access) is untouched.
When a plugin being installed declares dependencies that are missing or
disabled, the installer lists them (with their version ranges) before
downloading. The user can always continue - nothing is ever installed
automatically.
Each plugin's row in settings shows a small chip per declared dependency
with a check or cross depending on whether that dependency is installed
and enabled. Plugins without dependencies are unaffected.
…a theme

A theme's skin.json can declare the same optional "dependencies" field
(plugin names, optionally with version ranges). Installing a theme with
unmet plugin dependencies shows the same warning dialog as plugins do.
Closes SteamClientHomebrew#102
- warn once per distinct dependency problem instead of once per session
- reuse the name index from plugin_deps instead of scanning the plugin
  list for every dependency
- keep inclusive and exclusive version ranges distinct in the UI
  (">=1.2.0" reads as "min version", ">1.2.0" as "newer than")
- a crashed dependency no longer shows as satisfied in the plugin list
- include <cstddef> in plugin_deps.h instead of relying on transitive
  includes
Adds a "requires" field next to "dependencies", for plugins and themes
alike, covering the case where the dependency isn't optional.

Both fields take part in load ordering, but a required one is also
enforced: a plugin that requires another can't be enabled until it's
there, and a theme that requires plugins can't be applied. The toggle
in settings stays off with a tooltip naming what is missing, and the
native side refuses the same requests as a backstop for hand-edited
configs.

Turning off a plugin that others require asks first, then turns those
off as well (and drops back to the default theme if the active one
required it), so nothing ever runs without something it requires.
Installing is never blocked - the install dialog just marks required
dependencies as such, and nothing is ever installed automatically.
@WkdXeqtr

WkdXeqtr commented Aug 8, 2026

Copy link
Copy Markdown
Contributor Author

Thanks, all five addressed:

  1. warnings are deduplicated per problem (plugin + dependency + reason) rather than latched for the whole session, so each distinct issue is reported once no matter how often the list gets sorted
  2. sort_by_dependencies builds the name index once through plugin_deps::index_by_name and shares it with the warning pass
  3. = and > (and <= / <) render differently now - "min version 1.2.0" vs "newer than 1.2.0"

  4. chips treat a crashed dependency as unmet, since the config still says enabled but nothing is actually loaded
  5. added the include

Also took a shot at the hard variant, in its own commit so it can be dropped independently. "requires" sits next to "dependencies", npm style:

"requires": ["some-plugin"],
"dependencies": ["nice-to-have"]

Both order the load, but a required one is enforced: a plugin can't be enabled until its requirements are there, and a theme that lists requirements can't be applied. The toggle stays off with a tooltip naming what's missing, and the native side refuses the same requests so a hand-edited config can't sneak past. Turning off a plugin others require asks first and turns those off too, dropping back to the default theme if the active one required it - otherwise the guarantee would only hold until someone flipped the dependency off.

Installing stays permissive either way, the dialog just labels those entries "required", since installing something you can't enable yet is harmless.

Rebased onto current next.

@WkdXeqtr
WkdXeqtr marked this pull request as ready for review August 29, 2026 04:02
@WkdXeqtr

WkdXeqtr commented Aug 29, 2026

Copy link
Copy Markdown
Contributor Author

Marking this ready - the review notes are addressed and the hard variant is in as its own commit. I can split it up if the branch is too much at once.

A plugin built with starlight can declare "requires" and "dependencies"
in [plugin], and the ids are written into the .star metadata so the same
load ordering, install warning and enable rules apply to v2 plugins.

Millennium reads both fields back out of the metadata; before this only
"dependencies" was picked up, so a required dependency was silently
dropped for star plugins.
@WkdXeqtr

Copy link
Copy Markdown
Contributor Author

Realised the dependency work only covered v1 plugins, so this adds the v2 side: starlight writes "requires" and "dependencies" from [plugin] into the .star metadata, and Millennium reads both back out. Before this it only read "dependencies", so a required dependency was silently dropped for star plugins.

The [plugin] config fields also show up in #887 for the localization side. Whichever lands first, I'll rebase the other.

@ejalxndr

ejalxndr commented Sep 1, 2026

Copy link
Copy Markdown
Member

Honestly given that, maybe we should drop support entirely from V1 plugins, and only have it as a V2 feature? Would be much less to maintain. I'm open to discussion!

@WkdXeqtr

WkdXeqtr commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

Works for me - new plugins that want to declare dependencies can be expected to use the new format anyway, so I'll strip the schema change and the plugin.json reads.

Two things I'd keep: v1 plugins staying valid as targets, since the resolver matches by name whatever the format and an old plugin that never migrates can still be depended on - that lives in the shared core and costs nothing. And themes, since skins have no v2 format - that side reads skin.json either way.

Declaring "requires" and "dependencies" is now exclusive to the .star
manifest; the fields are ignored in a loose plugin.json and the schema
change is reverted. The resolver still matches targets by name whatever
their format, so a v2 plugin can depend on a v1 plugin, and themes keep
reading skin.json since skins have no v2 format.
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.

2 participants