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"`
|
Version string `toml:"version"`
|
||||||
Kind string `toml:"kind,omitempty"`
|
Kind string `toml:"kind,omitempty"`
|
||||||
Categories []string `toml:"categories,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
|
// Private marks the plugin as account-scoped. When true, Coords() returns
|
||||||
// the canonical "@private/<name>@<version>" form regardless of the Scope
|
// the canonical "@private/<name>@<version>" form regardless of the Scope
|
||||||
// field, and the publish flow attributes the plugin to the publisher's
|
// 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) {
|
func TestParseModFull_RequiresAndCompat(t *testing.T) {
|
||||||
src := []byte(`
|
src := []byte(`
|
||||||
[plugin]
|
[plugin]
|
||||||
|
|||||||
@ -45,4 +45,10 @@ type PluginRegistration struct {
|
|||||||
Dependencies []Dependency
|
Dependencies []Dependency
|
||||||
Migrations func() fs.FS
|
Migrations func() fs.FS
|
||||||
Version string
|
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"
|
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.
|
// CompareVersions compares two semver strings.
|
||||||
// Returns -1 if v1 < v2, 0 if equal, +1 if v1 > v2.
|
// Returns -1 if v1 < v2, 0 if equal, +1 if v1 > v2.
|
||||||
// Returns 0 if either version is invalid.
|
// Returns 0 if either version is invalid.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user