From 25f9d26dc59af03ff780d85ef531d7ffbda858e2 Mon Sep 17 00:00:00 2001 From: Soner Sayakci Date: Thu, 26 Mar 2026 15:27:42 +0100 Subject: [PATCH 1/2] fix(account): update extension label and description on info pull The pull command now writes label and short description from the store back to composer.json (for plugins) and manifest.xml (for apps), making push and pull symmetric operations. Fixes #929 --- .../account_producer_extension_info_pull.go | 22 ++++ internal/extension/app.go | 70 +++++++++++- internal/extension/bundle.go | 12 +- internal/extension/extension_test.go | 6 +- internal/extension/platform.go | 65 ++++++++++- internal/extension/root.go | 11 +- internal/extension/root_test.go | 103 ++++++++++++++++++ 7 files changed, 271 insertions(+), 18 deletions(-) diff --git a/cmd/account/account_producer_extension_info_pull.go b/cmd/account/account_producer_extension_info_pull.go index 1bb70c6a..e34a3d12 100644 --- a/cmd/account/account_producer_extension_info_pull.go +++ b/cmd/account/account_producer_extension_info_pull.go @@ -123,11 +123,17 @@ var accountCompanyProducerExtensionInfoPullCmd = &cobra.Command{ englishMetaTitle := "" germanMetaDescription := "" englishMetaDescription := "" + germanLabel := "" + englishLabel := "" + germanShortDescription := "" + englishShortDescription := "" for _, info := range storeExt.Infos { language := info.Locale.Name[0:2] if language == "de" { + germanLabel = info.Name + germanShortDescription = info.ShortDescription germanDescription = "file:src/Resources/store/description.de.html" germanInstallationManual = "file:src/Resources/store/installation_manual.de.html" germanMetaTitle = info.MetaTitle @@ -160,6 +166,8 @@ var accountCompanyProducerExtensionInfoPullCmd = &cobra.Command{ faqDE = append(faqDE, extension.ConfigStoreFaq{Question: element.Question, Answer: element.Answer, Position: element.Position}) } } else { + englishLabel = info.Name + englishShortDescription = info.ShortDescription englishDescription = "file:src/Resources/store/description.en.html" englishInstallationManual = "file:src/Resources/store/installation_manual.en.html" englishMetaTitle = info.MetaTitle @@ -195,6 +203,20 @@ var accountCompanyProducerExtensionInfoPullCmd = &cobra.Command{ } } + err = zipExt.UpdateMetaData(&extension.ExtensionMetadata{ + Label: extension.ExtensionTranslated{ + German: germanLabel, + English: englishLabel, + }, + Description: extension.ExtensionTranslated{ + German: germanShortDescription, + English: englishShortDescription, + }, + }) + if err != nil { + return fmt.Errorf("cannot update extension metadata: %w", err) + } + extType := "extension" if storeExt.ProductType != nil { diff --git a/internal/extension/app.go b/internal/extension/app.go index 12655e3c..d4f75425 100644 --- a/internal/extension/app.go +++ b/internal/extension/app.go @@ -136,22 +136,84 @@ func (a App) GetIconPath() string { return filepath.Join(a.GetRootDir(), iconPath) } -func (a App) GetMetaData() *extensionMetadata { +func (a App) GetMetaData() *ExtensionMetadata { german := []string{"de-DE", "de"} english := []string{"en-GB", "en-US", "en", ""} - return &extensionMetadata{ - Label: extensionTranslated{ + return &ExtensionMetadata{ + Label: ExtensionTranslated{ German: a.manifest.Meta.Label.GetValueByLanguage(german), English: a.manifest.Meta.Label.GetValueByLanguage(english), }, - Description: extensionTranslated{ + Description: ExtensionTranslated{ German: a.manifest.Meta.Description.GetValueByLanguage(german), English: a.manifest.Meta.Description.GetValueByLanguage(english), }, } } +func (a App) UpdateMetaData(metadata *ExtensionMetadata) error { + manifestFile := fmt.Sprintf("%s/manifest.xml", a.path) + + manifestBytes, err := os.ReadFile(manifestFile) + if err != nil { + return fmt.Errorf("could not read manifest.xml: %w", err) + } + + var manifest Manifest + if err := xml.Unmarshal(manifestBytes, &manifest); err != nil { + return fmt.Errorf("could not parse manifest.xml: %w", err) + } + + manifest.Meta.Label = updateTranslatableString(manifest.Meta.Label, metadata.Label) + manifest.Meta.Description = updateTranslatableString(manifest.Meta.Description, metadata.Description) + + newXml, err := xml.MarshalIndent(manifest, "", " ") + if err != nil { + return fmt.Errorf("could not marshal manifest.xml: %w", err) + } + + newXml = append([]byte(xml.Header), newXml...) + + if err := os.WriteFile(manifestFile, newXml, os.ModePerm); err != nil { + return fmt.Errorf("could not write manifest.xml: %w", err) + } + + return nil +} + +func updateTranslatableString(existing TranslatableString, translated ExtensionTranslated) TranslatableString { + langMap := map[string]string{ + "de-DE": translated.German, + "en-GB": translated.English, + } + + for i, entry := range existing { + if val, ok := langMap[entry.Lang]; ok && val != "" { + existing[i].Value = val + delete(langMap, entry.Lang) + } + // default language entry (no lang attr) maps to en-GB + if entry.Lang == "" { + if val, ok := langMap["en-GB"]; ok && val != "" { + existing[i].Value = val + delete(langMap, "en-GB") + } + } + } + + for lang, val := range langMap { + if val != "" { + existing = append(existing, struct { + Value string `xml:",chardata"` + Lang string `xml:"lang,attr,omitempty"` + }{Value: val, Lang: lang}) + } + } + + return existing +} + func (a App) Validate(_ context.Context, check validation.Check) { validateTheme(a, check) diff --git a/internal/extension/bundle.go b/internal/extension/bundle.go index da1c248a..3ff3889b 100644 --- a/internal/extension/bundle.go +++ b/internal/extension/bundle.go @@ -146,19 +146,23 @@ func (p ShopwareBundle) GetIconPath() string { return "" } -func (p ShopwareBundle) GetMetaData() *extensionMetadata { - return &extensionMetadata{ - Label: extensionTranslated{ +func (p ShopwareBundle) GetMetaData() *ExtensionMetadata { + return &ExtensionMetadata{ + Label: ExtensionTranslated{ German: "FALLBACK", English: "FALLBACK", }, - Description: extensionTranslated{ + Description: ExtensionTranslated{ German: "FALLBACK", English: "FALLBACK", }, } } +func (p ShopwareBundle) UpdateMetaData(_ *ExtensionMetadata) error { + return nil +} + func (p ShopwareBundle) Validate(c context.Context, check validation.Check) { // ShopwareBundle validation is currently empty but signature updated to match interface } diff --git a/internal/extension/extension_test.go b/internal/extension/extension_test.go index 92a9d7cf..a7fa7ef6 100644 --- a/internal/extension/extension_test.go +++ b/internal/extension/extension_test.go @@ -84,7 +84,11 @@ func (m *mockExtension) GetChangelog() (*ExtensionChangelog, error) { return &ExtensionChangelog{}, nil } -func (m *mockExtension) GetMetaData() *extensionMetadata { +func (m *mockExtension) GetMetaData() *ExtensionMetadata { + return nil +} + +func (m *mockExtension) UpdateMetaData(_ *ExtensionMetadata) error { return nil } diff --git a/internal/extension/platform.go b/internal/extension/platform.go index 44976a1b..89420a11 100644 --- a/internal/extension/platform.go +++ b/internal/extension/platform.go @@ -160,20 +160,77 @@ func (p PlatformPlugin) GetPath() string { return p.path } -func (p PlatformPlugin) GetMetaData() *extensionMetadata { - return &extensionMetadata{ +func (p PlatformPlugin) GetMetaData() *ExtensionMetadata { + return &ExtensionMetadata{ Name: p.Composer.Name, - Label: extensionTranslated{ + Label: ExtensionTranslated{ German: p.Composer.Extra.Label["de-DE"], English: p.Composer.Extra.Label["en-GB"], }, - Description: extensionTranslated{ + Description: ExtensionTranslated{ German: p.Composer.Extra.Description["de-DE"], English: p.Composer.Extra.Description["en-GB"], }, } } +func (p PlatformPlugin) UpdateMetaData(metadata *ExtensionMetadata) error { + composerJsonFile := fmt.Sprintf("%s/composer.json", p.path) + + composerJson, err := os.ReadFile(composerJsonFile) + if err != nil { + return fmt.Errorf("could not read composer.json: %w", err) + } + + var composerJsonStruct map[string]interface{} + if err := json.Unmarshal(composerJson, &composerJsonStruct); err != nil { + return fmt.Errorf("could not unmarshal composer.json: %w", err) + } + + extra, ok := composerJsonStruct["extra"].(map[string]interface{}) + if !ok { + extra = make(map[string]interface{}) + composerJsonStruct["extra"] = extra + } + + label, ok := extra["label"].(map[string]interface{}) + if !ok { + label = make(map[string]interface{}) + } + if metadata.Label.German != "" { + label["de-DE"] = metadata.Label.German + } + if metadata.Label.English != "" { + label["en-GB"] = metadata.Label.English + } + extra["label"] = label + + description, ok := extra["description"].(map[string]interface{}) + if !ok { + description = make(map[string]interface{}) + } + if metadata.Description.German != "" { + description["de-DE"] = metadata.Description.German + } + if metadata.Description.English != "" { + description["en-GB"] = metadata.Description.English + } + extra["description"] = description + + newComposerJson, err := json.MarshalIndent(composerJsonStruct, "", " ") + if err != nil { + return fmt.Errorf("could not marshal composer.json: %w", err) + } + + newComposerJson = append(newComposerJson, '\n') + + if err := os.WriteFile(composerJsonFile, newComposerJson, os.ModePerm); err != nil { + return fmt.Errorf("could not write composer.json: %w", err) + } + + return nil +} + func (p PlatformPlugin) GetIconPath() string { pluginIcon := p.Composer.Extra.PluginIcon diff --git a/internal/extension/root.go b/internal/extension/root.go index 0c18e550..0aa5c99d 100644 --- a/internal/extension/root.go +++ b/internal/extension/root.go @@ -83,7 +83,7 @@ func GetExtensionByZip(ctx context.Context, filePath string) (Extension, error) return GetExtensionByFolder(ctx, fmt.Sprintf("%s/%s", dir, extName)) } -type extensionTranslated struct { +type ExtensionTranslated struct { German string `json:"german"` English string `json:"english"` } @@ -94,10 +94,10 @@ type ExtensionChangelog struct { Changelogs map[string]string } -type extensionMetadata struct { +type ExtensionMetadata struct { Name string - Label extensionTranslated - Description extensionTranslated + Label ExtensionTranslated + Description ExtensionTranslated } type Extension interface { @@ -118,7 +118,8 @@ type Extension interface { GetType() string GetPath() string GetChangelog() (*ExtensionChangelog, error) - GetMetaData() *extensionMetadata + GetMetaData() *ExtensionMetadata + UpdateMetaData(*ExtensionMetadata) error GetExtensionConfig() *Config Validate(context.Context, validation.Check) } diff --git a/internal/extension/root_test.go b/internal/extension/root_test.go index d1665b6e..f8dcac43 100644 --- a/internal/extension/root_test.go +++ b/internal/extension/root_test.go @@ -169,6 +169,109 @@ func TestGetExtensionByFolder_PrefersManifestOverComposer(t *testing.T) { assert.Equal(t, TypePlatformApp, ext.GetType()) } +func TestUpdateMetaData_PlatformPlugin(t *testing.T) { + tmpDir := t.TempDir() + + composerContent := `{ + "name": "test/test-plugin", + "type": "shopware-platform-plugin", + "version": "1.0.0", + "license": "MIT", + "description": "Test plugin", + "authors": [{"name": "Test"}], + "require": { + "shopware/core": "~6.5.0" + }, + "autoload": { + "psr-4": { + "Test\\TestPlugin\\": "src/" + } + }, + "extra": { + "shopware-plugin-class": "Test\\TestPlugin\\TestPlugin", + "label": { + "de-DE": "Altes Label DE", + "en-GB": "Old Label EN" + }, + "description": { + "de-DE": "Alte Beschreibung", + "en-GB": "Old description" + } + } +}` + require.NoError(t, os.WriteFile(filepath.Join(tmpDir, "composer.json"), []byte(composerContent), 0644)) + + ext, err := GetExtensionByFolder(t.Context(), tmpDir) + require.NoError(t, err) + + err = ext.UpdateMetaData(&ExtensionMetadata{ + Label: ExtensionTranslated{ + German: "Neues Label DE", + English: "New Label EN", + }, + Description: ExtensionTranslated{ + German: "Neue Beschreibung", + English: "New description", + }, + }) + require.NoError(t, err) + + // Re-read the extension to verify the changes were persisted + ext2, err := GetExtensionByFolder(t.Context(), tmpDir) + require.NoError(t, err) + + meta := ext2.GetMetaData() + assert.Equal(t, "Neues Label DE", meta.Label.German) + assert.Equal(t, "New Label EN", meta.Label.English) + assert.Equal(t, "Neue Beschreibung", meta.Description.German) + assert.Equal(t, "New description", meta.Description.English) +} + +func TestUpdateMetaData_App(t *testing.T) { + tmpDir := t.TempDir() + + manifestContent := ` + + + TestApp + + + Old description + Alte Beschreibung + Test Author + (c) Test + 1.0.0 + MIT + +` + require.NoError(t, os.WriteFile(filepath.Join(tmpDir, "manifest.xml"), []byte(manifestContent), 0644)) + + ext, err := GetExtensionByFolder(t.Context(), tmpDir) + require.NoError(t, err) + + err = ext.UpdateMetaData(&ExtensionMetadata{ + Label: ExtensionTranslated{ + German: "Neues Label DE", + English: "New Label EN", + }, + Description: ExtensionTranslated{ + German: "Neue Beschreibung", + English: "New description", + }, + }) + require.NoError(t, err) + + // Re-read the extension to verify the changes were persisted + ext2, err := GetExtensionByFolder(t.Context(), tmpDir) + require.NoError(t, err) + + meta := ext2.GetMetaData() + assert.Equal(t, "Neues Label DE", meta.Label.German) + assert.Equal(t, "New Label EN", meta.Label.English) + assert.Equal(t, "Neue Beschreibung", meta.Description.German) + assert.Equal(t, "New description", meta.Description.English) +} + func TestGetShopwareVersionConstraintFromComposer(t *testing.T) { t.Run("uses config constraint when set", func(t *testing.T) { config := &Config{ From af3885730f9b4cfe09ab5d52464a57bac22bbf54 Mon Sep 17 00:00:00 2001 From: Soner Sayakci Date: Thu, 26 Mar 2026 16:06:38 +0100 Subject: [PATCH 2/2] fix: address review feedback for metadata pull - Use deterministic order when appending new translations in manifest.xml - Guard UpdateMetaData call to skip when all fields are empty - Only write label/description keys to composer.json when values exist - Skip composer.json write entirely when nothing changed --- .../account_producer_extension_info_pull.go | 26 +++++----- internal/extension/app.go | 33 ++++++------ internal/extension/platform.go | 50 ++++++++++++------- 3 files changed, 63 insertions(+), 46 deletions(-) diff --git a/cmd/account/account_producer_extension_info_pull.go b/cmd/account/account_producer_extension_info_pull.go index e34a3d12..3cb4d951 100644 --- a/cmd/account/account_producer_extension_info_pull.go +++ b/cmd/account/account_producer_extension_info_pull.go @@ -203,18 +203,20 @@ var accountCompanyProducerExtensionInfoPullCmd = &cobra.Command{ } } - err = zipExt.UpdateMetaData(&extension.ExtensionMetadata{ - Label: extension.ExtensionTranslated{ - German: germanLabel, - English: englishLabel, - }, - Description: extension.ExtensionTranslated{ - German: germanShortDescription, - English: englishShortDescription, - }, - }) - if err != nil { - return fmt.Errorf("cannot update extension metadata: %w", err) + if germanLabel != "" || englishLabel != "" || germanShortDescription != "" || englishShortDescription != "" { + err = zipExt.UpdateMetaData(&extension.ExtensionMetadata{ + Label: extension.ExtensionTranslated{ + German: germanLabel, + English: englishLabel, + }, + Description: extension.ExtensionTranslated{ + German: germanShortDescription, + English: englishShortDescription, + }, + }) + if err != nil { + return fmt.Errorf("cannot update extension metadata: %w", err) + } } extType := "extension" diff --git a/internal/extension/app.go b/internal/extension/app.go index d4f75425..b4acc078 100644 --- a/internal/extension/app.go +++ b/internal/extension/app.go @@ -183,31 +183,34 @@ func (a App) UpdateMetaData(metadata *ExtensionMetadata) error { } func updateTranslatableString(existing TranslatableString, translated ExtensionTranslated) TranslatableString { - langMap := map[string]string{ - "de-DE": translated.German, - "en-GB": translated.English, + translations := []struct { + lang string + value string + }{ + {"en-GB", translated.English}, + {"de-DE", translated.German}, } + matched := make(map[string]bool) + for i, entry := range existing { - if val, ok := langMap[entry.Lang]; ok && val != "" { - existing[i].Value = val - delete(langMap, entry.Lang) - } - // default language entry (no lang attr) maps to en-GB - if entry.Lang == "" { - if val, ok := langMap["en-GB"]; ok && val != "" { - existing[i].Value = val - delete(langMap, "en-GB") + for _, t := range translations { + if t.value == "" { + continue + } + if entry.Lang == t.lang || (entry.Lang == "" && t.lang == "en-GB") { + existing[i].Value = t.value + matched[t.lang] = true } } } - for lang, val := range langMap { - if val != "" { + for _, t := range translations { + if t.value != "" && !matched[t.lang] { existing = append(existing, struct { Value string `xml:",chardata"` Lang string `xml:"lang,attr,omitempty"` - }{Value: val, Lang: lang}) + }{Value: t.value, Lang: t.lang}) } } diff --git a/internal/extension/platform.go b/internal/extension/platform.go index 89420a11..dc89e42f 100644 --- a/internal/extension/platform.go +++ b/internal/extension/platform.go @@ -193,29 +193,41 @@ func (p PlatformPlugin) UpdateMetaData(metadata *ExtensionMetadata) error { composerJsonStruct["extra"] = extra } - label, ok := extra["label"].(map[string]interface{}) - if !ok { - label = make(map[string]interface{}) - } - if metadata.Label.German != "" { - label["de-DE"] = metadata.Label.German - } - if metadata.Label.English != "" { - label["en-GB"] = metadata.Label.English - } - extra["label"] = label + changed := false - description, ok := extra["description"].(map[string]interface{}) - if !ok { - description = make(map[string]interface{}) + if metadata.Label.German != "" || metadata.Label.English != "" { + label, ok := extra["label"].(map[string]interface{}) + if !ok { + label = make(map[string]interface{}) + } + if metadata.Label.German != "" { + label["de-DE"] = metadata.Label.German + } + if metadata.Label.English != "" { + label["en-GB"] = metadata.Label.English + } + extra["label"] = label + changed = true } - if metadata.Description.German != "" { - description["de-DE"] = metadata.Description.German + + if metadata.Description.German != "" || metadata.Description.English != "" { + description, ok := extra["description"].(map[string]interface{}) + if !ok { + description = make(map[string]interface{}) + } + if metadata.Description.German != "" { + description["de-DE"] = metadata.Description.German + } + if metadata.Description.English != "" { + description["en-GB"] = metadata.Description.English + } + extra["description"] = description + changed = true } - if metadata.Description.English != "" { - description["en-GB"] = metadata.Description.English + + if !changed { + return nil } - extra["description"] = description newComposerJson, err := json.MarshalIndent(composerJsonStruct, "", " ") if err != nil {