From 2a01af4b2a22a526d9d292745abe343aab7a7b3b Mon Sep 17 00:00:00 2001 From: Anne Hintzpeter Date: Mon, 14 Sep 2026 08:06:24 +0200 Subject: [PATCH 1/4] feat: add create theme flag --- internal/extension/create.go | 12 ++- internal/extension/create_test.go | 62 +++++++++---- internal/extension/scaffolding/scaffolding.go | 88 +++++++++++++++---- .../extension/scaffolding/scaffolding_test.go | 69 ++++++++++++++- .../scaffolding/stubs/theme.json.tmpl | 22 +++++ .../scaffolding/stubs/theme_class.php.tmpl | 10 +++ .../stubs/theme_composer.json.tmpl | 21 +++++ .../stubs/theme_overrides.scss.tmpl | 8 ++ 8 files changed, 255 insertions(+), 37 deletions(-) create mode 100644 internal/extension/scaffolding/stubs/theme.json.tmpl create mode 100644 internal/extension/scaffolding/stubs/theme_class.php.tmpl create mode 100644 internal/extension/scaffolding/stubs/theme_composer.json.tmpl create mode 100644 internal/extension/scaffolding/stubs/theme_overrides.scss.tmpl diff --git a/internal/extension/create.go b/internal/extension/create.go index 4503a5c9..98faf916 100644 --- a/internal/extension/create.go +++ b/internal/extension/create.go @@ -32,6 +32,16 @@ func Create(ctx context.Context, opts CreateOptions) (err error) { logger.Info("Creating extension...") + var createFiles func(string, string, string) error + switch opts.Type { + case Plugin: + createFiles = scaffolding.CreatePluginFiles + case Theme: + createFiles = scaffolding.CreateThemeFiles + default: + return fmt.Errorf("unsupported extension type %q", opts.Type) + } + projectDir, err := shop.FindClosestShopwareProject(false) if err != nil { return err @@ -56,7 +66,7 @@ func Create(ctx context.Context, opts CreateOptions) (err error) { } }() - if err = scaffolding.CreateExtensionFiles(extensionDir, opts.Name, opts.Vendor); err != nil { + if err = createFiles(extensionDir, opts.Name, opts.Vendor); err != nil { return fmt.Errorf("create extension files: %w", err) } diff --git a/internal/extension/create_test.go b/internal/extension/create_test.go index 686284ab..746c134f 100644 --- a/internal/extension/create_test.go +++ b/internal/extension/create_test.go @@ -80,26 +80,54 @@ func TestCreateFailsOutsideShopwareProject(t *testing.T) { } func TestCreateGeneratesAnExtension(t *testing.T) { - for _, store := range []bool{false, true} { - t.Run(fmt.Sprintf("store=%t", store), func(t *testing.T) { - projectDir := newProject(t) - opts := validCreateOptions() - opts.Store = store - - require.NoError(t, Create(t.Context(), opts)) - - technicalName := deriveTechnicalName(opts.Name, opts.Vendor) - extensionDir := deriveExtensionDirectoryName(projectDir, opts.Store, technicalName) - assert.FileExists(t, filepath.Join(extensionDir, "composer.json")) - assert.FileExists(t, filepath.Join(extensionDir, "src", "Resources", "config", "config.xml")) - assert.FileExists(t, filepath.Join(extensionDir, ".gitignore")) - assert.FileExists(t, filepath.Join(extensionDir, "phpunit.xml")) - assert.FileExists(t, filepath.Join(extensionDir, "src", technicalName+".php")) - assert.FileExists(t, filepath.Join(extensionDir, "tests", "TestBootstrap.php")) - }) + for _, extensionType := range []ExtensionType{Plugin, Theme} { + for _, store := range []bool{false, true} { + t.Run(fmt.Sprintf("type=%s/store=%t", extensionType, store), func(t *testing.T) { + projectDir := newProject(t) + opts := validCreateOptions() + opts.Type = extensionType + opts.Store = store + + require.NoError(t, Create(t.Context(), opts)) + + technicalName := deriveTechnicalName(opts.Name, opts.Vendor) + extensionDir := deriveExtensionDirectoryName(projectDir, opts.Store, technicalName) + assert.FileExists(t, filepath.Join(extensionDir, "composer.json")) + assert.FileExists(t, filepath.Join(extensionDir, "src", technicalName+".php")) + + if extensionType == Plugin { + assert.FileExists(t, filepath.Join(extensionDir, "src", "Resources", "config", "config.xml")) + assert.FileExists(t, filepath.Join(extensionDir, ".gitignore")) + assert.FileExists(t, filepath.Join(extensionDir, "phpunit.xml")) + assert.FileExists(t, filepath.Join(extensionDir, "tests", "TestBootstrap.php")) + assert.NoFileExists(t, filepath.Join(extensionDir, "src", "Resources", "theme.json")) + return + } + + assert.FileExists(t, filepath.Join(extensionDir, "src", "Resources", "theme.json")) + assert.FileExists(t, filepath.Join( + extensionDir, + "src/Resources/app/storefront/src/scss/overrides.scss", + )) + assert.NoFileExists(t, filepath.Join(extensionDir, "phpunit.xml")) + }) + } } } +func TestCreateRejectsUnsupportedTypeWithoutCreatingDirectory(t *testing.T) { + projectDir := newProject(t) + opts := validCreateOptions() + opts.Type = "app" + technicalName := deriveTechnicalName(opts.Name, opts.Vendor) + extensionDir := deriveExtensionDirectoryName(projectDir, opts.Store, technicalName) + + err := Create(t.Context(), opts) + + assert.ErrorContains(t, err, `unsupported extension type "app"`) + assert.NoDirExists(t, extensionDir) +} + func validCreateOptions() CreateOptions { return CreateOptions{ Name: "MyExtension", diff --git a/internal/extension/scaffolding/scaffolding.go b/internal/extension/scaffolding/scaffolding.go index fa60eacc..4a9b1659 100644 --- a/internal/extension/scaffolding/scaffolding.go +++ b/internal/extension/scaffolding/scaffolding.go @@ -45,8 +45,8 @@ type scaffoldingFile struct { StubPath string } -// scaffoldingFiles returns a list of files with their paths and corresponding stub paths. -func scaffoldingFiles(className string) []scaffoldingFile { +// pluginScaffoldingFiles returns the files in a platform plugin scaffold. +func pluginScaffoldingFiles(className string) []scaffoldingFile { return []scaffoldingFile{ { Path: "composer.json", @@ -75,6 +75,44 @@ func scaffoldingFiles(className string) []scaffoldingFile { } } +// themeScaffoldingFiles returns the default files generated for a storefront theme. +func themeScaffoldingFiles(data scaffoldData) []scaffoldingFile { + return []scaffoldingFile{ + { + Path: "composer.json", + StubPath: "stubs/theme_composer.json.tmpl", + }, + { + Path: filepath.Join("src", data.ClassName+".php"), + StubPath: "stubs/theme_class.php.tmpl", + }, + { + Path: "src/Resources/theme.json", + StubPath: "stubs/theme.json.tmpl", + }, + { + Path: "src/Resources/app/storefront/src/scss/overrides.scss", + StubPath: "stubs/theme_overrides.scss.tmpl", + }, + { + Path: "src/Resources/app/storefront/src/scss/base.scss", + }, + { + Path: "src/Resources/app/storefront/src/assets/.gitkeep", + }, + { + Path: "src/Resources/app/storefront/src/main.js", + }, + { + Path: filepath.Join( + "src/Resources/app/storefront/dist/storefront/js", + data.AssetName, + data.AssetName+".js", + ), + }, + } +} + // CreateExtensionDir creates an empty extension directory. Its parents must already exist. func CreateExtensionDir(extensionDir string) error { info, err := os.Stat(extensionDir) @@ -107,10 +145,20 @@ func CreateExtensionDir(extensionDir string) error { return nil } -// CreateExtensionFiles creates all scaffolding Files that are given back by scaffoldingFiles() -func CreateExtensionFiles(extensionDir, extensionName, vendorName string) error { +// CreatePluginFiles creates the files for a platform plugin. +func CreatePluginFiles(extensionDir, extensionName, vendorName string) error { data := createScaffoldingData(vendorName, extensionName) - for _, file := range scaffoldingFiles(data.ClassName) { + return createExtensionFiles(extensionDir, pluginScaffoldingFiles(data.ClassName), data) +} + +// CreateThemeFiles creates the files for a storefront theme. +func CreateThemeFiles(extensionDir, extensionName, vendorName string) error { + data := createScaffoldingData(vendorName, extensionName) + return createExtensionFiles(extensionDir, themeScaffoldingFiles(data), data) +} + +func createExtensionFiles(extensionDir string, files []scaffoldingFile, data scaffoldData) error { + for _, file := range files { err := createFileWithScaffolding(extensionDir, file, data) if err != nil { return err @@ -120,7 +168,7 @@ func CreateExtensionFiles(extensionDir, extensionName, vendorName string) error return nil } -// createFileWithScaffolding renders one embedded template into an existing extension. +// createFileWithScaffolding renders an embedded template or creates an empty placeholder. func createFileWithScaffolding(extensionDir string, file scaffoldingFile, data scaffoldData) (err error) { dest := filepath.Join(extensionDir, file.Path) if err := os.MkdirAll(filepath.Dir(dest), 0o755); err != nil { @@ -137,18 +185,20 @@ func createFileWithScaffolding(extensionDir string, file scaffoldingFile, data s } }() - stubBytes, err := stubsFS.ReadFile(file.StubPath) - if err != nil { - return fmt.Errorf("read stub file: %w", err) - } + if file.StubPath != "" { + stubBytes, err := stubsFS.ReadFile(file.StubPath) + if err != nil { + return fmt.Errorf("read stub file: %w", err) + } - tmpl, err := template.New(file.Path).Funcs(stubFuncs).Parse(string(stubBytes)) - if err != nil { - return fmt.Errorf("parse stub: %w", err) - } + tmpl, err := template.New(file.Path).Funcs(stubFuncs).Parse(string(stubBytes)) + if err != nil { + return fmt.Errorf("parse stub: %w", err) + } - if err := tmpl.Execute(f, data); err != nil { - return fmt.Errorf("render: %w", err) + if err := tmpl.Execute(f, data); err != nil { + return fmt.Errorf("render: %w", err) + } } if err := f.Sync(); err != nil { return fmt.Errorf("flush file to disk: %w", err) @@ -161,13 +211,17 @@ type scaffoldData struct { Namespace string ClassName string ComposerName string + AssetName string } func createScaffoldingData(vendorName string, extensionName string) scaffoldData { + className := DeriveClassName(vendorName, extensionName) + return scaffoldData{ Namespace: DeriveNamespace(vendorName, extensionName), - ClassName: DeriveClassName(vendorName, extensionName), + ClassName: className, ComposerName: DeriveComposerName(vendorName, extensionName), + AssetName: strings.ToLower(strings.Join(splitPascalCase(className), "-")), } } diff --git a/internal/extension/scaffolding/scaffolding_test.go b/internal/extension/scaffolding/scaffolding_test.go index 3a2c0e71..63b43833 100644 --- a/internal/extension/scaffolding/scaffolding_test.go +++ b/internal/extension/scaffolding/scaffolding_test.go @@ -89,13 +89,13 @@ func TestCreateExtensionDirErrors(t *testing.T) { }) } -func TestCreateExtensionFiles(t *testing.T) { +func TestCreatePluginFiles(t *testing.T) { projectDir := newProject(t) technicalName := "MyVendorMyExtension" extensionDir := filepath.Join(projectDir, "custom", "plugins", technicalName) require.NoError(t, os.MkdirAll(extensionDir, 0o755)) - require.NoError(t, CreateExtensionFiles(extensionDir, "MyExtension", "MyVendor")) + require.NoError(t, CreatePluginFiles(extensionDir, "MyExtension", "MyVendor")) assert.DirExists(t, filepath.Join(extensionDir, "src", "Resources", "config")) assert.DirExists(t, filepath.Join(extensionDir, "tests")) @@ -109,6 +109,71 @@ func TestCreateExtensionFiles(t *testing.T) { assert.FileExists(t, filepath.Join(extensionDir, "tests", "TestBootstrap.php")) } +func TestCreateThemeFiles(t *testing.T) { + projectDir := newProject(t) + technicalName := "MyVendorMyExtension" + assetName := "my-vendor-my-extension" + extensionDir := filepath.Join(projectDir, "custom", "plugins", technicalName) + require.NoError(t, os.MkdirAll(extensionDir, 0o755)) + + require.NoError(t, CreateThemeFiles(extensionDir, "MyExtension", "MyVendor")) + + expectedFiles := []string{ + "composer.json", + filepath.Join("src", technicalName+".php"), + "src/Resources/theme.json", + "src/Resources/app/storefront/src/scss/overrides.scss", + "src/Resources/app/storefront/src/scss/base.scss", + "src/Resources/app/storefront/src/assets/.gitkeep", + "src/Resources/app/storefront/src/main.js", + filepath.Join( + "src/Resources/app/storefront/dist/storefront/js", + assetName, + assetName+".js", + ), + } + for _, file := range expectedFiles { + assert.FileExists(t, filepath.Join(extensionDir, file)) + } + for _, file := range []string{ + "src/Resources/app/storefront/src/scss/base.scss", + "src/Resources/app/storefront/src/assets/.gitkeep", + "src/Resources/app/storefront/src/main.js", + filepath.Join( + "src/Resources/app/storefront/dist/storefront/js", + assetName, + assetName+".js", + ), + } { + info, err := os.Stat(filepath.Join(extensionDir, file)) + require.NoError(t, err) + assert.Zero(t, info.Size()) + } + + assert.NoFileExists(t, filepath.Join(extensionDir, "phpunit.xml")) + assert.NoFileExists(t, filepath.Join(extensionDir, "src", "Resources", "config", "config.xml")) + + composer, err := os.ReadFile(filepath.Join(extensionDir, "composer.json")) + require.NoError(t, err) + assert.Contains(t, string(composer), `"shopware-plugin-class": "MyVendor\\MyExtension\\MyVendorMyExtension"`) + + bootstrap, err := os.ReadFile(filepath.Join(extensionDir, "src", technicalName+".php")) + require.NoError(t, err) + assert.Contains(t, string(bootstrap), `namespace MyVendor\MyExtension;`) + assert.Contains(t, string(bootstrap), "implements ThemeInterface") + + themeConfig, err := os.ReadFile(filepath.Join(extensionDir, "src", "Resources", "theme.json")) + require.NoError(t, err) + assert.JSONEq(t, `{ + "name": "MyVendorMyExtension", + "author": "Shopware AG", + "views": ["@Storefront", "@Plugins", "@MyVendorMyExtension"], + "style": ["app/storefront/src/scss/overrides.scss", "@Storefront", "app/storefront/src/scss/base.scss"], + "script": ["@Storefront", "app/storefront/dist/storefront/js/my-vendor-my-extension/my-vendor-my-extension.js"], + "asset": ["@Storefront", "app/storefront/src/assets"] + }`, string(themeConfig)) +} + func TestCreateFileWithScaffoldingErrors(t *testing.T) { t.Run("destination file already exists", func(t *testing.T) { extensionDir := filepath.Join(t.TempDir(), "MyVendorMyExtension") diff --git a/internal/extension/scaffolding/stubs/theme.json.tmpl b/internal/extension/scaffolding/stubs/theme.json.tmpl new file mode 100644 index 00000000..5b3c556c --- /dev/null +++ b/internal/extension/scaffolding/stubs/theme.json.tmpl @@ -0,0 +1,22 @@ +{ + "name": "{{ .ClassName }}", + "author": "Shopware AG", + "views": [ + "@Storefront", + "@Plugins", + "@{{ .ClassName }}" + ], + "style": [ + "app/storefront/src/scss/overrides.scss", + "@Storefront", + "app/storefront/src/scss/base.scss" + ], + "script": [ + "@Storefront", + "app/storefront/dist/storefront/js/{{ .AssetName }}/{{ .AssetName }}.js" + ], + "asset": [ + "@Storefront", + "app/storefront/src/assets" + ] +} diff --git a/internal/extension/scaffolding/stubs/theme_class.php.tmpl b/internal/extension/scaffolding/stubs/theme_class.php.tmpl new file mode 100644 index 00000000..41f1194c --- /dev/null +++ b/internal/extension/scaffolding/stubs/theme_class.php.tmpl @@ -0,0 +1,10 @@ + Date: Thu, 24 Sep 2026 17:09:49 +0200 Subject: [PATCH 2/4] fix: add theme stubs folder --- internal/extension/scaffolding/scaffolding.go | 14 ++-- .../extension/scaffolding/scaffolding_test.go | 68 ++++--------------- .../stubs/{ => theme}/theme.json.tmpl | 2 +- .../stubs/{ => theme}/theme_class.php.tmpl | 0 .../{ => theme}/theme_composer.json.tmpl | 9 +-- .../{ => theme}/theme_overrides.scss.tmpl | 0 6 files changed, 28 insertions(+), 65 deletions(-) rename internal/extension/scaffolding/stubs/{ => theme}/theme.json.tmpl (94%) rename internal/extension/scaffolding/stubs/{ => theme}/theme_class.php.tmpl (100%) rename internal/extension/scaffolding/stubs/{ => theme}/theme_composer.json.tmpl (66%) rename internal/extension/scaffolding/stubs/{ => theme}/theme_overrides.scss.tmpl (100%) diff --git a/internal/extension/scaffolding/scaffolding.go b/internal/extension/scaffolding/scaffolding.go index 4a9b1659..41ac260f 100644 --- a/internal/extension/scaffolding/scaffolding.go +++ b/internal/extension/scaffolding/scaffolding.go @@ -80,19 +80,19 @@ func themeScaffoldingFiles(data scaffoldData) []scaffoldingFile { return []scaffoldingFile{ { Path: "composer.json", - StubPath: "stubs/theme_composer.json.tmpl", + StubPath: "stubs/theme/theme_composer.json.tmpl", }, { Path: filepath.Join("src", data.ClassName+".php"), - StubPath: "stubs/theme_class.php.tmpl", + StubPath: "stubs/theme/theme_class.php.tmpl", }, { Path: "src/Resources/theme.json", - StubPath: "stubs/theme.json.tmpl", + StubPath: "stubs/theme/theme.json.tmpl", }, { Path: "src/Resources/app/storefront/src/scss/overrides.scss", - StubPath: "stubs/theme_overrides.scss.tmpl", + StubPath: "stubs/theme/theme_overrides.scss.tmpl", }, { Path: "src/Resources/app/storefront/src/scss/base.scss", @@ -221,7 +221,7 @@ func createScaffoldingData(vendorName string, extensionName string) scaffoldData Namespace: DeriveNamespace(vendorName, extensionName), ClassName: className, ComposerName: DeriveComposerName(vendorName, extensionName), - AssetName: strings.ToLower(strings.Join(splitPascalCase(className), "-")), + AssetName: DeriveAssetName(className), } } @@ -256,6 +256,10 @@ func DeriveClassName(vendorName string, extensionName string) string { return vendorName + extensionName } +func DeriveAssetName(className string) string { + return strings.ToLower(strings.Join(splitPascalCase(className), "-")) +} + // splitPascalCase is a helper function and splits a PascalCase string into its constituent words. func splitPascalCase(name string) []string { if name == "" { diff --git a/internal/extension/scaffolding/scaffolding_test.go b/internal/extension/scaffolding/scaffolding_test.go index 63b43833..17ece996 100644 --- a/internal/extension/scaffolding/scaffolding_test.go +++ b/internal/extension/scaffolding/scaffolding_test.go @@ -117,61 +117,19 @@ func TestCreateThemeFiles(t *testing.T) { require.NoError(t, os.MkdirAll(extensionDir, 0o755)) require.NoError(t, CreateThemeFiles(extensionDir, "MyExtension", "MyVendor")) - - expectedFiles := []string{ - "composer.json", - filepath.Join("src", technicalName+".php"), - "src/Resources/theme.json", - "src/Resources/app/storefront/src/scss/overrides.scss", - "src/Resources/app/storefront/src/scss/base.scss", - "src/Resources/app/storefront/src/assets/.gitkeep", - "src/Resources/app/storefront/src/main.js", - filepath.Join( - "src/Resources/app/storefront/dist/storefront/js", - assetName, - assetName+".js", - ), - } - for _, file := range expectedFiles { - assert.FileExists(t, filepath.Join(extensionDir, file)) - } - for _, file := range []string{ - "src/Resources/app/storefront/src/scss/base.scss", - "src/Resources/app/storefront/src/assets/.gitkeep", - "src/Resources/app/storefront/src/main.js", - filepath.Join( - "src/Resources/app/storefront/dist/storefront/js", - assetName, - assetName+".js", - ), - } { - info, err := os.Stat(filepath.Join(extensionDir, file)) - require.NoError(t, err) - assert.Zero(t, info.Size()) - } - - assert.NoFileExists(t, filepath.Join(extensionDir, "phpunit.xml")) - assert.NoFileExists(t, filepath.Join(extensionDir, "src", "Resources", "config", "config.xml")) - - composer, err := os.ReadFile(filepath.Join(extensionDir, "composer.json")) - require.NoError(t, err) - assert.Contains(t, string(composer), `"shopware-plugin-class": "MyVendor\\MyExtension\\MyVendorMyExtension"`) - - bootstrap, err := os.ReadFile(filepath.Join(extensionDir, "src", technicalName+".php")) - require.NoError(t, err) - assert.Contains(t, string(bootstrap), `namespace MyVendor\MyExtension;`) - assert.Contains(t, string(bootstrap), "implements ThemeInterface") - - themeConfig, err := os.ReadFile(filepath.Join(extensionDir, "src", "Resources", "theme.json")) - require.NoError(t, err) - assert.JSONEq(t, `{ - "name": "MyVendorMyExtension", - "author": "Shopware AG", - "views": ["@Storefront", "@Plugins", "@MyVendorMyExtension"], - "style": ["app/storefront/src/scss/overrides.scss", "@Storefront", "app/storefront/src/scss/base.scss"], - "script": ["@Storefront", "app/storefront/dist/storefront/js/my-vendor-my-extension/my-vendor-my-extension.js"], - "asset": ["@Storefront", "app/storefront/src/assets"] - }`, string(themeConfig)) + // all expected files for a storefront theme are created + assert.FileExists(t, filepath.Join(extensionDir, "composer.json")) + assert.FileExists(t, filepath.Join(extensionDir, "src", technicalName+".php")) + assert.FileExists(t, filepath.Join(extensionDir, "src", "Resources", "theme.json")) + assert.FileExists(t, filepath.Join(extensionDir, "src", "Resources", "app", "storefront", "src", "scss", "overrides.scss")) + assert.FileExists(t, filepath.Join(extensionDir, "src", "Resources", "app", "storefront", "src", "scss", "base.scss")) + assert.FileExists(t, filepath.Join(extensionDir, "src", "Resources", "app", "storefront", "src", "assets", ".gitkeep")) + assert.FileExists(t, filepath.Join(extensionDir, "src", "Resources", "app", "storefront", "src", "main.js")) + assert.FileExists(t, filepath.Join(extensionDir, "src", "Resources", "app", "storefront", "dist", "storefront", "js", assetName, assetName+".js")) + // all expected directories for a storefront theme are created + assert.DirExists(t, filepath.Join(extensionDir, "src", "Resources", "app", "storefront", "src", "scss")) + assert.DirExists(t, filepath.Join(extensionDir, "src", "Resources", "app", "storefront", "src", "assets")) + assert.DirExists(t, filepath.Join(extensionDir, "src", "Resources", "app", "storefront", "dist", "storefront", "js", assetName)) } func TestCreateFileWithScaffoldingErrors(t *testing.T) { diff --git a/internal/extension/scaffolding/stubs/theme.json.tmpl b/internal/extension/scaffolding/stubs/theme/theme.json.tmpl similarity index 94% rename from internal/extension/scaffolding/stubs/theme.json.tmpl rename to internal/extension/scaffolding/stubs/theme/theme.json.tmpl index 5b3c556c..78ef596e 100644 --- a/internal/extension/scaffolding/stubs/theme.json.tmpl +++ b/internal/extension/scaffolding/stubs/theme/theme.json.tmpl @@ -1,6 +1,6 @@ { "name": "{{ .ClassName }}", - "author": "Shopware AG", + "author": "Acme", "views": [ "@Storefront", "@Plugins", diff --git a/internal/extension/scaffolding/stubs/theme_class.php.tmpl b/internal/extension/scaffolding/stubs/theme/theme_class.php.tmpl similarity index 100% rename from internal/extension/scaffolding/stubs/theme_class.php.tmpl rename to internal/extension/scaffolding/stubs/theme/theme_class.php.tmpl diff --git a/internal/extension/scaffolding/stubs/theme_composer.json.tmpl b/internal/extension/scaffolding/stubs/theme/theme_composer.json.tmpl similarity index 66% rename from internal/extension/scaffolding/stubs/theme_composer.json.tmpl rename to internal/extension/scaffolding/stubs/theme/theme_composer.json.tmpl index 7fccde7a..2214bd9b 100644 --- a/internal/extension/scaffolding/stubs/theme_composer.json.tmpl +++ b/internal/extension/scaffolding/stubs/theme/theme_composer.json.tmpl @@ -1,8 +1,9 @@ { "name": "{{ .ComposerName }}", - "description": "Theme skeleton plugin", + "description": "{{ .ComposerName }}", "type": "shopware-platform-plugin", - "license": "MIT", + "license": "proprietary", + "version": "0.1.0", "require": { "shopware/core": "~6.7.0" }, @@ -14,8 +15,8 @@ "extra": { "shopware-plugin-class": "{{ jsonEscape .Namespace }}\\{{ .ClassName }}", "label": { - "de-DE": "Theme {{ .ClassName }} plugin", - "en-GB": "Theme {{ .ClassName }} plugin" + "de-DE": "{{ .ClassName }} Theme", + "en-GB": "{{ .ClassName }} Theme" } } } diff --git a/internal/extension/scaffolding/stubs/theme_overrides.scss.tmpl b/internal/extension/scaffolding/stubs/theme/theme_overrides.scss.tmpl similarity index 100% rename from internal/extension/scaffolding/stubs/theme_overrides.scss.tmpl rename to internal/extension/scaffolding/stubs/theme/theme_overrides.scss.tmpl From b81ba942b780e5587566f3e28178968e5a7cb3bd Mon Sep 17 00:00:00 2001 From: Anne Hintzpeter Date: Thu, 24 Sep 2026 18:33:19 +0200 Subject: [PATCH 3/4] fix: delete unnecessary test --- internal/extension/create_test.go | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/internal/extension/create_test.go b/internal/extension/create_test.go index 746c134f..f796e37e 100644 --- a/internal/extension/create_test.go +++ b/internal/extension/create_test.go @@ -115,19 +115,6 @@ func TestCreateGeneratesAnExtension(t *testing.T) { } } -func TestCreateRejectsUnsupportedTypeWithoutCreatingDirectory(t *testing.T) { - projectDir := newProject(t) - opts := validCreateOptions() - opts.Type = "app" - technicalName := deriveTechnicalName(opts.Name, opts.Vendor) - extensionDir := deriveExtensionDirectoryName(projectDir, opts.Store, technicalName) - - err := Create(t.Context(), opts) - - assert.ErrorContains(t, err, `unsupported extension type "app"`) - assert.NoDirExists(t, extensionDir) -} - func validCreateOptions() CreateOptions { return CreateOptions{ Name: "MyExtension", From 2a451db3e0eb9a8d0323e88f3241d688b0f19303 Mon Sep 17 00:00:00 2001 From: Anne Hintzpeter Date: Thu, 24 Sep 2026 19:53:05 +0200 Subject: [PATCH 4/4] fix: improve test coverage and remove type flag default --- cmd/extension/extension_create.go | 2 +- internal/extension/create_test.go | 77 ++++++++++++++++++++++++++++--- 2 files changed, 71 insertions(+), 8 deletions(-) diff --git a/cmd/extension/extension_create.go b/cmd/extension/extension_create.go index c8fa4be6..634268ac 100644 --- a/cmd/extension/extension_create.go +++ b/cmd/extension/extension_create.go @@ -72,7 +72,7 @@ func newCreateCmd() *cobra.Command { flags.StringVar(&opts.Name, NameFlagName, "", "Extension name (PascalCase)") flags.StringVar(&opts.Vendor, VendorFlagName, "", "Vendor prefix (PascalCase) for the extension name and namespace. Required if --store is enabled.") flags.BoolVar(&opts.Store, StoreFlagName, false, "Enable if you plan to publish the extension on the Shopware Community Store.") - flags.StringVarP((*string)(&opts.Type), TypeFlagName, "t", string(extension.Plugin), "Extension type (plugin|theme)") + flags.StringVarP((*string)(&opts.Type), TypeFlagName, "t", "", "Extension type (plugin|theme)") _ = cmd.RegisterFlagCompletionFunc("type", cobra.FixedCompletions( []string{string(extension.Plugin), string(extension.Theme)}, diff --git a/internal/extension/create_test.go b/internal/extension/create_test.go index f796e37e..91a23b30 100644 --- a/internal/extension/create_test.go +++ b/internal/extension/create_test.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "path/filepath" + "strings" "testing" "github.com/stretchr/testify/assert" @@ -70,15 +71,77 @@ func TestValidateExtensionType(t *testing.T) { } } -func TestCreateFailsOutsideShopwareProject(t *testing.T) { - t.Setenv("PROJECT_ROOT", "") - t.Chdir(t.TempDir()) - - err := Create(system.WithInteraction(t.Context(), false), validCreateOptions()) - - assert.ErrorContains(t, err, "cannot find Shopware project") +func TestCreateErrors(t *testing.T) { + t.Run("unsupported extension type", func(t *testing.T) { + opts := validCreateOptions() + opts.Type = "not-a-type" + + assert.EqualError(t, Create(t.Context(), opts), `unsupported extension type "not-a-type"`) + }) + + t.Run("cannot find Shopware project", func(t *testing.T) { + t.Setenv("PROJECT_ROOT", "") + t.Chdir(t.TempDir()) + + err := Create(system.WithInteraction(t.Context(), false), validCreateOptions()) + + assert.ErrorContains(t, err, "cannot find Shopware project") + }) + + t.Run("extension directory already exists", func(t *testing.T) { + projectDir := newProject(t) + opts := validCreateOptions() + extensionDir := deriveExtensionDirectoryName(projectDir, opts.Store, deriveTechnicalName(opts.Name, opts.Vendor)) + require.NoError(t, os.Mkdir(extensionDir, 0o755)) + + assert.ErrorContains(t, Create(t.Context(), opts), "already exists") + }) + + t.Run("path exists as file", func(t *testing.T) { + projectDir := newProject(t) + opts := validCreateOptions() + extensionDir := deriveExtensionDirectoryName(projectDir, opts.Store, deriveTechnicalName(opts.Name, opts.Vendor)) + require.NoError(t, os.WriteFile(extensionDir, nil, 0o644)) + + assert.ErrorContains(t, Create(t.Context(), opts), "not a directory") + }) + + t.Run("plugin root does not exist", func(t *testing.T) { + projectDir := newProject(t) + opts := validCreateOptions() + require.NoError(t, os.RemoveAll(filepath.Join(projectDir, "custom"))) + extensionDir := deriveExtensionDirectoryName(projectDir, opts.Store, deriveTechnicalName(opts.Name, opts.Vendor)) + + assert.ErrorContains(t, Create(t.Context(), opts), "does not exist") + assert.NoDirExists(t, extensionDir) + }) + + t.Run("parent path not a directory", func(t *testing.T) { + projectDir := newProject(t) + opts := validCreateOptions() + parentPath := filepath.Join(projectDir, "custom", "static-plugins") + require.NoError(t, os.RemoveAll(parentPath)) + require.NoError(t, os.WriteFile(parentPath, nil, 0o644)) + extensionDir := deriveExtensionDirectoryName(projectDir, opts.Store, deriveTechnicalName(opts.Name, opts.Vendor)) + + assert.ErrorContains(t, Create(t.Context(), opts), "not a directory") + assert.NoDirExists(t, extensionDir) + }) + + t.Run("extension files cannot be written", func(t *testing.T) { + projectDir := newProject(t) + opts := validCreateOptions() + opts.Vendor = "V" + // Directory name stays under NAME_MAX (255); the plugin class file does not. + opts.Name = strings.Repeat("A", 251) + extensionDir := deriveExtensionDirectoryName(projectDir, opts.Store, deriveTechnicalName(opts.Name, opts.Vendor)) + + assert.ErrorContains(t, Create(t.Context(), opts), "create extension files:") + assert.NoDirExists(t, extensionDir) + }) } + func TestCreateGeneratesAnExtension(t *testing.T) { for _, extensionType := range []ExtensionType{Plugin, Theme} { for _, store := range []bool{false, true} {