revenge-bundle-next/lib/plugins/src/_internal/dependency-graph.ts
PalmDevs 9b412a9404
feat!: new repo system + versioning system support + bug fixes
BREAKING CHANGES:
- New versioning system: Allowing any amount of number segments, with prerelease support.
  - Any number of segments is supported: 1, 1.0, 1.0.0, 1.0.0.0, ...
  - Prerelease labels are compared "digit-run", similar to Debian.
  - Plugins must specify a version.
  - User-facing plugins should use CalVer. API plugins should use SemVer.
    Revenge APIs use SemVer while the new `discord` dependency parses the Discord version string directly.
- Dependency is now a Map, with support for version ranges and optional. Suggested URL is removed. (See new repo system)
- Plugins will now have to declare a dependency on `revenge.api` (Revenge API version) and `discord` (Discord version).
- Manifest now requires a format version (currently `1`)

New APIs:
- lib/plugins/utils: Utilities to format versioning

Features:
- New repo system: Repository design is inspired by Linux package repositories.
  - Repos host manifest, versions, channels, download URL/size, and hash for multiple plugins.
  - New dependency system: Plugins are versioned, and shouldn't break when updates come.
    Plugins can now declare dependency on supported Discord/Revenge API versions, so they won't break with auto-updates.
    Dependencies can be optional, but dependency on `revenge.api` and `discord` must be required.
    Dependents are soft-disabled when dependencies don't satisfy. Once satisfied, the plugin can start again.
  - All updates for running plugins REQUIRE a reload to prevent stale linking issues. Both native and JS-side.
  - Update checking is handled entirely by JS. But the native-side still handles installation, indexing, and verification.
  - Local indexes are updated against remote indexes. Dependency checking always uses local indexes.
  - Users can add custom repositories and custom their priority order. URL is the identity.
- Plugins can now have custom data URI icons instead of being restricted to React Native app assets.
- External plugin installs require UI confirmation before installing.
- Pending Update and Pending Reload filters in Plugins page.
- Ability to browse external plugins in repositories. Official repository will be added soon.

Fixes/Refactor:
- Refactored Plugin-related settings to a separate "Plugin Settings" plugin.
- Masonry layout fixes in landscape.
- Updated plugins now show "reload to apply" toast instead.
- Removed dead code.
2026-07-25 23:43:44 +07:00

90 lines
3 KiB
TypeScript

import { getPluginDependencies, isPluginStartable } from '../_internal'
import { pApis } from './decorators'
import type { AnyPlugin } from '../_internal'
/// PLUGIN DEPENDENCY GRAPHING
// We don't store the graph as a tree, but rather as a set of nodes.
// Root nodes are plugins that have dependencies, but no dependents. These plugins are the starting points of the dependency graph.
// Leaf nodes are plugins that have no dependencies, but may have dependents. These plugins are the end points of the dependency graph.
// Start order: Single nodes (no dependencies & dependents) -> Leaf nodes (no dependencies, maybe dependents) -> Root nodes (with dependencies, no dependents)
// This way we can ensure that all dependencies are started before the plugin itself.
export const pRootNodes = new Set<AnyPlugin>()
export const pLeafOrSingleNodes = new Set<AnyPlugin>()
// Visited non-leaf nodes
const visited = new Set<AnyPlugin>()
// Ordered list of plugins to be started
export const pListOrdered: AnyPlugin[] = []
// Pending plugins to be computed
export const pPending = new Set<AnyPlugin>()
// Reserved dependencies verified by native
export const ApiDependencyId = 'revenge.api'
export const DiscordDependencyId = 'discord'
export function isReservedDependency(id: string) {
return id === ApiDependencyId || id === DiscordDependencyId
}
/**
* Whether the plugin has start-order dependencies. Reserved dependencies don't count.
*/
function hasGraphableDependencies(plugin: AnyPlugin): boolean {
const deps = plugin.manifest.dependencies
if (!deps) return false
for (const id in deps) if (!isReservedDependency(id)) return true
return false
}
export function computePendingNodes() {
for (const plugin of pPending)
if (isPluginStartable(plugin)) resolvePluginGraph(plugin)
const apis: AnyPlugin[] = []
for (const plugin of pLeafOrSingleNodes) {
if (pApis.has(plugin)) apis.push(plugin)
else pListOrdered.unshift(plugin)
}
for (const plugin of apis) pListOrdered.unshift(plugin)
const stack = [...pRootNodes]
while (stack.length) {
const plugin = stack.shift()!
if (visited.has(plugin)) {
pListOrdered.push(plugin)
continue
}
if (hasGraphableDependencies(plugin)) {
for (const dep of getPluginDependencies(plugin, false))
if (!pLeafOrSingleNodes.has(dep)) stack.push(dep)
stack.push(plugin)
visited.add(plugin)
} else pListOrdered.push(plugin)
}
pPending.clear()
pLeafOrSingleNodes.clear()
pRootNodes.clear()
visited.clear()
}
export function resolvePluginGraph(plugin: AnyPlugin) {
if (hasGraphableDependencies(plugin)) {
// Optimisitically add to root nodes (if there are dependents, it will be removed later)
pRootNodes.add(plugin)
// Not a root node if it has dependencies
for (const dep of getPluginDependencies(plugin, false))
if (hasGraphableDependencies(dep)) pRootNodes.delete(dep)
} else pLeafOrSingleNodes.add(plugin)
}