diff --git a/plugin/mod.go b/plugin/mod.go index a66d2b9..adc0ca8 100644 --- a/plugin/mod.go +++ b/plugin/mod.go @@ -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/@" form regardless of the Scope // field, and the publish flow attributes the plugin to the publisher's diff --git a/plugin/mod_test.go b/plugin/mod_test.go index 05b79cc..798f4ec 100644 --- a/plugin/mod_test.go +++ b/plugin/mod_test.go @@ -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] diff --git a/plugin/registration.go b/plugin/registration.go index 7935804..389c537 100644 --- a/plugin/registration.go +++ b/plugin/registration.go @@ -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 } diff --git a/plugin/version.go b/plugin/version.go index e357da7..bda87e5 100644 --- a/plugin/version.go +++ b/plugin/version.go @@ -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.