Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cmd/extension/extension_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)},
Expand Down
12 changes: 11 additions & 1 deletion internal/extension/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
}

Expand Down
126 changes: 102 additions & 24 deletions internal/extension/create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"fmt"
"os"
"path/filepath"
"strings"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -70,33 +71,110 @@
}
}

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)
})
}


Check failure on line 144 in internal/extension/create_test.go

View workflow job for this annotation

GitHub Actions / lint

File is not properly formatted (gci)
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"))
})
}
}
}

Expand Down
92 changes: 75 additions & 17 deletions internal/extension/scaffolding/scaffolding.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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/theme_composer.json.tmpl",
},
{
Path: filepath.Join("src", data.ClassName+".php"),
StubPath: "stubs/theme/theme_class.php.tmpl",
},
{
Path: "src/Resources/theme.json",
StubPath: "stubs/theme/theme.json.tmpl",
},
{
Path: "src/Resources/app/storefront/src/scss/overrides.scss",
StubPath: "stubs/theme/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",
Comment thread
MalteJanz marked this conversation as resolved.
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)
Expand Down Expand Up @@ -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)
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)
for _, file := range scaffoldingFiles(data.ClassName) {
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
Expand All @@ -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 {
Expand All @@ -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)
Expand All @@ -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: DeriveAssetName(className),
}
}

Expand Down Expand Up @@ -202,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 == "" {
Expand Down
27 changes: 25 additions & 2 deletions internal/extension/scaffolding/scaffolding_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
Expand All @@ -109,6 +109,29 @@ 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"))
// 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) {
t.Run("destination file already exists", func(t *testing.T) {
extensionDir := filepath.Join(t.TempDir(), "MyVendorMyExtension")
Expand Down
22 changes: 22 additions & 0 deletions internal/extension/scaffolding/stubs/theme/theme.json.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"name": "{{ .ClassName }}",
"author": "Acme",
"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"
]
}
Loading
Loading