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>
This commit is contained in:
parent
c3c7b2d441
commit
ba87684696
@ -30,6 +30,13 @@ type ModPlugin struct {
|
||||
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
|
||||
|
||||
@ -169,6 +169,73 @@ func TestCoords_PrivateNoScope(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseModFull_RequiredIconPacks(t *testing.T) {
|
||||
src := []byte(`
|
||||
[plugin]
|
||||
name = "neon"
|
||||
version = "0.1.0"
|
||||
required_icon_packs = ["tabler", "phosphor"]
|
||||
`)
|
||||
m, err := ParseModFull(src)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseModFull err: %v", err)
|
||||
}
|
||||
if len(m.Plugin.RequiredIconPacks) != 2 {
|
||||
t.Fatalf("RequiredIconPacks len = %d, want 2", len(m.Plugin.RequiredIconPacks))
|
||||
}
|
||||
if m.Plugin.RequiredIconPacks[0] != "tabler" || m.Plugin.RequiredIconPacks[1] != "phosphor" {
|
||||
t.Errorf("RequiredIconPacks = %v", m.Plugin.RequiredIconPacks)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseModFull_RequiredIconPacksOmitted(t *testing.T) {
|
||||
src := []byte(`
|
||||
[plugin]
|
||||
name = "noicons"
|
||||
version = "0.1.0"
|
||||
`)
|
||||
m, err := ParseModFull(src)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseModFull err: %v", err)
|
||||
}
|
||||
if len(m.Plugin.RequiredIconPacks) != 0 {
|
||||
t.Errorf("RequiredIconPacks should be empty when omitted, got %v", m.Plugin.RequiredIconPacks)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseRequiredIconPacks(t *testing.T) {
|
||||
src := []byte(`
|
||||
[plugin]
|
||||
name = "neon"
|
||||
version = "0.1.0"
|
||||
required_icon_packs = ["tabler", " phosphor ", ""]
|
||||
`)
|
||||
got := ParseRequiredIconPacks(src)
|
||||
if len(got) != 2 {
|
||||
t.Fatalf("ParseRequiredIconPacks len = %d (%v), want 2", len(got), got)
|
||||
}
|
||||
if got[0] != "tabler" || got[1] != "phosphor" {
|
||||
t.Errorf("ParseRequiredIconPacks = %v, want [tabler phosphor]", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseRequiredIconPacks_NilOnAbsent(t *testing.T) {
|
||||
src := []byte(`
|
||||
[plugin]
|
||||
name = "noicons"
|
||||
version = "0.1.0"
|
||||
`)
|
||||
if got := ParseRequiredIconPacks(src); got != nil {
|
||||
t.Errorf("ParseRequiredIconPacks on absent field = %v, want nil", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseRequiredIconPacks_NilOnInvalidTOML(t *testing.T) {
|
||||
if got := ParseRequiredIconPacks([]byte("not valid = = =")); got != nil {
|
||||
t.Errorf("ParseRequiredIconPacks on invalid TOML = %v, want nil", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseModFull_RequiresAndCompat(t *testing.T) {
|
||||
src := []byte(`
|
||||
[plugin]
|
||||
|
||||
@ -45,4 +45,10 @@ type PluginRegistration struct {
|
||||
Dependencies []Dependency
|
||||
Migrations func() fs.FS
|
||||
Version string
|
||||
|
||||
// RequiredIconPacks are icon-pack slugs the CMS must ensure are installed
|
||||
// before the theme loads (e.g. "tabler", "phosphor"). Loader auto-installs
|
||||
// from the bundled registry; out-of-registry slugs are logged and require
|
||||
// manual install.
|
||||
RequiredIconPacks []string
|
||||
}
|
||||
|
||||
@ -31,6 +31,33 @@ func ParseModVersion(data []byte) string {
|
||||
return "0.0.0"
|
||||
}
|
||||
|
||||
// ParseRequiredIconPacks extracts the required_icon_packs list from an embedded
|
||||
// plugin.mod file. Returns nil if the field is absent or empty. Mirrors the
|
||||
// "read straight from TOML bytes" style of ParseModVersion so plugin
|
||||
// registration.go files can populate PluginRegistration.RequiredIconPacks
|
||||
// without having to duplicate the slugs in Go.
|
||||
func ParseRequiredIconPacks(data []byte) []string {
|
||||
m, err := ParseModFull(data)
|
||||
if err != nil || m == nil {
|
||||
return nil
|
||||
}
|
||||
if len(m.Plugin.RequiredIconPacks) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make([]string, 0, len(m.Plugin.RequiredIconPacks))
|
||||
for _, slug := range m.Plugin.RequiredIconPacks {
|
||||
s := strings.TrimSpace(slug)
|
||||
if s == "" {
|
||||
continue
|
||||
}
|
||||
out = append(out, s)
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return nil
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// CompareVersions compares two semver strings.
|
||||
// Returns -1 if v1 < v2, 0 if equal, +1 if v1 > v2.
|
||||
// Returns 0 if either version is invalid.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user