core/plugin/mod.go
Alex Dunmow ba87684696 feat(plugin): add RequiredIconPacks to PluginRegistration and ModPlugin
Lets plugins declare icon-pack dependencies (e.g. "tabler", "phosphor")
in plugin.mod and PluginRegistration. The CMS loader auto-installs
declared packs from the bundled registry before the plugin loads.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-07 15:14:15 +08:00

89 lines
3.4 KiB
Go

package plugin
import (
"strings"
tomlpkg "github.com/BurntSushi/toml"
)
type ModFile struct {
Plugin ModPlugin `toml:"plugin"`
Compatibility *ModCompat `toml:"compatibility"`
Requires []ModRequirement `toml:"requires"`
}
type ModPlugin struct {
// Name is the lowercase identifier used for the plugin slug in URLs and DB
// lookups. The CLI normalises this on write; the registry normalises on
// create. Use DisplayName for human-readable presentation.
Name string `toml:"name"`
// DisplayName is the human-readable form (any case). Optional; if empty
// the registry falls back to the input name with its original case.
DisplayName string `toml:"display_name,omitempty"`
// Description is the short summary surfaced in the registry. Optional.
Description string `toml:"description,omitempty"`
// Scope is the plugin owner namespace as it appears in plugin.mod. It may
// include the leading "@" (e.g. "@themes") or omit it (e.g. "themes") —
// both forms are accepted. Consumers comparing scopes should trim the "@"
// before comparing; use ModFile.Coords() for a normalised display string.
Scope string `toml:"scope"`
Version string `toml:"version"`
Kind string `toml:"kind,omitempty"`
Categories []string `toml:"categories,omitempty"`
// RequiredIconPacks names icon-pack slugs the host CMS must ensure are
// installed before the plugin is loaded (e.g. "tabler", "phosphor"). The
// standalone-plugin loader honours this best-effort by auto-installing any
// missing packs from the bundled registry; slugs outside that whitelist are
// logged and skipped (admins install them manually). Empty / omitted means
// the plugin has no icon-pack dependencies.
RequiredIconPacks []string `toml:"required_icon_packs,omitempty"`
// Private marks the plugin as account-scoped. When true, Coords() returns
// the canonical "@private/<name>@<version>" form regardless of the Scope
// field, and the publish flow attributes the plugin to the publisher's
// active account rather than to a public scope.
Private bool `toml:"private,omitempty"`
}
type ModCompat struct {
BlockCore string `toml:"block_core"`
}
type ModRequirement struct {
Name string `toml:"name"`
Version string `toml:"version"`
}
func ParseModFull(b []byte) (*ModFile, error) {
var m ModFile
if err := tomlpkg.Unmarshal(b, &m); err != nil {
return nil, err
}
return &m, nil
}
// Coords returns the canonical display coordinate for the plugin in the form
// "@scope/name@version" (or "name@version" when no scope is set).
//
// The leading "@" on m.Plugin.Scope is intentionally trimmed before
// re-prefixing so that authors may write either "@themes" or "themes" in
// plugin.mod and get the same output. Callers that need the raw scope as
// written should read m.Plugin.Scope directly.
func (m *ModFile) Coords() string {
if m == nil {
return ""
}
if m.Plugin.Private {
return "@" + PrivateScopeSlug + "/" + m.Plugin.Name + "@" + m.Plugin.Version
}
scope := strings.TrimPrefix(m.Plugin.Scope, "@")
if scope == "" {
return m.Plugin.Name + "@" + m.Plugin.Version
}
return "@" + scope + "/" + m.Plugin.Name + "@" + m.Plugin.Version
}
// PrivateScopeSlug is the registry namespace under which all private plugins
// live. Coords for private plugins resolve to "@private/<name>@<version>";
// uniqueness is enforced by (owner_account_id, name), not by the slug.
const PrivateScopeSlug = "private"