diff --git a/plugin/mod.go b/plugin/mod.go index a1fbbf8..29fc4dc 100644 --- a/plugin/mod.go +++ b/plugin/mod.go @@ -13,7 +13,11 @@ type ModFile struct { } type ModPlugin struct { - Name string `toml:"name"` + Name string `toml:"name"` + // 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"` @@ -37,6 +41,13 @@ func ParseModFull(b []byte) (*ModFile, error) { 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 "" diff --git a/plugin/mod_test.go b/plugin/mod_test.go index a88368e..1c98585 100644 --- a/plugin/mod_test.go +++ b/plugin/mod_test.go @@ -100,6 +100,20 @@ version = "0.1.0" } } +func TestCoords_AcceptsScopeWithOrWithoutAt(t *testing.T) { + want := "@themes/foo@1.0.0" + + withAt := &ModFile{Plugin: ModPlugin{Name: "foo", Scope: "@themes", Version: "1.0.0"}} + if got := withAt.Coords(); got != want { + t.Errorf("Coords() with leading @ = %q, want %q", got, want) + } + + withoutAt := &ModFile{Plugin: ModPlugin{Name: "foo", Scope: "themes", Version: "1.0.0"}} + if got := withoutAt.Coords(); got != want { + t.Errorf("Coords() without leading @ = %q, want %q", got, want) + } +} + func TestParseModFull_RequiresAndCompat(t *testing.T) { src := []byte(` [plugin]