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
105 changes: 26 additions & 79 deletions cmd/project/platform.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
package project

import (
"context"
"encoding/json"
"fmt"
"os"
"os/exec"
"path"
"path/filepath"
"slices"
"strings"
Expand Down Expand Up @@ -72,35 +68,7 @@ func filterAndWritePluginJson(cmd *cobra.Command, projectRoot string, shopCfg *s
return err
}

assetConfig := extension.AssetBuildConfig{
ShopwareRoot: projectRoot,
Executor: cmdExecutor,
}

cfgs := extension.BuildAssetConfigFromExtensions(cmd.Context(), sources, assetConfig)

if _, err := extension.InstallNodeModulesOfConfigs(cmd.Context(), cfgs, assetConfig); err != nil {
return err
}

// Normalize paths for the execution environment (e.g. Docker container).
for _, cfg := range cfgs {
cfg.BasePath = cmdExecutor.NormalizePath(cfg.BasePath)
for i, v := range cfg.Views {
cfg.Views[i] = cmdExecutor.NormalizePath(v)
}
}

pluginJson, err := json.MarshalIndent(cfgs, "", " ")
if err != nil {
return err
}

if err := os.WriteFile(path.Join(projectRoot, "var", "plugins.json"), pluginJson, os.ModePerm); err != nil {
return err
}

return nil
return extension.WritePluginJsonForSources(cmd.Context(), projectRoot, sources, cmdExecutor)
}

func filterAndGetSources(cmd *cobra.Command, projectRoot string, shopCfg *shop.Config) ([]asset.Source, error) {
Expand All @@ -109,9 +77,7 @@ func filterAndGetSources(cmd *cobra.Command, projectRoot string, shopCfg *shop.C
return nil, err
}

sources, err := extension.DumpAndLoadAssetSourcesOfProject(executor.AllowBinCI(cmd.Context()), projectRoot, shopCfg, func(ctx context.Context, args ...string) *exec.Cmd {
return cmdExecutor.ConsoleCommand(ctx, args...).Cmd
})
sources, err := extension.LoadProjectAssetSources(cmd.Context(), projectRoot, shopCfg, cmdExecutor)
if err != nil {
return nil, err
}
Expand All @@ -124,87 +90,68 @@ func filterAndGetSources(cmd *cobra.Command, projectRoot string, shopCfg *shop.C
return nil, fmt.Errorf("only-extensions and skip-extensions cannot be used together")
}

logger := logging.FromContext(cmd.Context())

if onlyCustomStatic {
logging.FromContext(cmd.Context()).Infof("Only including extensions from custom/static-plugins directory")
logging.FromContext(cmd.Context()).Debugf("Found %d total extensions before filtering", len(sources))
logger.Infof("Only including extensions from custom/static-plugins directory")
logger.Debugf("Found %d total extensions before filtering", len(sources))
for _, s := range sources {
logging.FromContext(cmd.Context()).Debugf("Extension: %s, Path: %s", s.Name, s.Path)
logger.Debugf("Extension: %s, Path: %s", s.Name, s.Path)
}

sources = slices.DeleteFunc(sources, func(s asset.Source) bool {
// We want to always include the Storefront extension, otherwise the watchers have problems
// Storefront must stay or the watchers break.
if s.Name == storefrontBundleName {
return false
}

// First try to resolve any symlinks
resolvedPath, err := filepath.EvalSymlinks(s.Path)
if err != nil {
logging.FromContext(cmd.Context()).Errorf("Failed to resolve symlink for %s: %v", s.Path, err)
logger.Errorf("Failed to resolve symlink for %s: %v", s.Path, err)
return true
}

absPath, err := filepath.Abs(resolvedPath)
if err != nil {
logging.FromContext(cmd.Context()).Errorf("Failed to get absolute path for %s: %v", resolvedPath, err)
logger.Errorf("Failed to get absolute path for %s: %v", resolvedPath, err)
return true
}

logging.FromContext(cmd.Context()).Debugf("Extension %s: Original path: %s, Resolved absolute path: %s", s.Name, s.Path, absPath)
logger.Debugf("Extension %s: Original path: %s, Resolved absolute path: %s", s.Name, s.Path, absPath)

customStaticDir := filepath.Join("custom", "static-plugins")

isCustomStatic := strings.Contains(absPath, customStaticDir) || strings.HasSuffix(absPath, customStaticDir)

if !isCustomStatic {
logging.FromContext(cmd.Context()).Debugf("Excluding %s as it's not in custom/static-plugins", s.Name)
logger.Debugf("Excluding %s as it's not in custom/static-plugins", s.Name)
}
return !isCustomStatic
})

logging.FromContext(cmd.Context()).Debugf("Found %d custom/static extensions after filtering", len(sources))
logger.Debugf("Found %d custom/static extensions after filtering", len(sources))
for _, s := range sources {
logging.FromContext(cmd.Context()).Debugf("Included extension: %s, Path: %s", s.Name, s.Path)
}

logging.FromContext(cmd.Context()).Debugf("Included extensions:")
for _, s := range sources {
logging.FromContext(cmd.Context()).Debugf(" - %s", s.Name)
logger.Debugf("Included extension: %s, Path: %s", s.Name, s.Path)
}
}

if onlyExtensions == "" && skipExtensions == "" && !onlyCustomStatic {
logging.FromContext(cmd.Context()).Infof("Excluding extensions based on project config: %s", strings.Join(shopCfg.Build.ExcludeExtensions, ", "))
switch {
case onlyExtensions != "":
logger.Infof("Only including extensions: %s", onlyExtensions)
allowed := strings.Split(onlyExtensions, ",")
sources = slices.DeleteFunc(sources, func(s asset.Source) bool {
// We want to always include the Storefront extension, otherwise the watchers have problems
// Storefront must stay or the watchers break.
if s.Name == storefrontBundleName {
return false
}

return slices.Contains(shopCfg.Build.ExcludeExtensions, s.Name)
return !slices.Contains(allowed, s.Name)
})
}

if onlyExtensions != "" {
logging.FromContext(cmd.Context()).Infof("Only including extensions: %s", onlyExtensions)
sources = slices.DeleteFunc(sources, func(s asset.Source) bool {
// We want to always include the Storefront extension, otherwise the watchers have problems
if s.Name == storefrontBundleName {
return false
}

return !slices.Contains(strings.Split(onlyExtensions, ","), s.Name)
})
} else if skipExtensions != "" {
logging.FromContext(cmd.Context()).Infof("Excluding extensions: %s", skipExtensions)
sources = slices.DeleteFunc(sources, func(s asset.Source) bool {
// We want to always include the Storefront extension, otherwise the watchers have problems
if s.Name == storefrontBundleName {
return false
}
case skipExtensions != "":
logger.Infof("Excluding extensions: %s", skipExtensions)
sources = extension.ExcludeExtensionsFromSources(sources, strings.Split(skipExtensions, ","))

return slices.Contains(strings.Split(skipExtensions, ","), s.Name)
})
case !onlyCustomStatic:
logger.Infof("Excluding extensions based on project config: %s", strings.Join(shopCfg.Build.ExcludeExtensions, ", "))
sources = extension.ExcludeExtensionsFromSources(sources, shopCfg.Build.ExcludeExtensions)
}

return sources, nil
Expand Down
6 changes: 5 additions & 1 deletion internal/devtui/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ func New(opts Options) Model {

return Model{
activeTab: tabGeneral,
general: NewGeneralModel(opts.Executor.Type(), shopURL, username, password, opts.ProjectRoot, opts.Executor),
general: NewGeneralModel(opts.Executor.Type(), shopURL, username, password, opts.ProjectRoot, opts.Executor, opts.Config),
logs: NewLogsModel(opts.ProjectRoot, isDocker),
configTab: NewConfigModel(opts.Config),
dockerMode: isDocker,
Expand Down Expand Up @@ -208,6 +208,10 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.general.sfWatchRunning = false
}
delete(m.watchers, msg.name)
if msg.err != nil {
m.logs.AppendErrorLine(msg.name + " failed to start: " + msg.err.Error())
m.activeTab = tabLogs
}
return m, nil

case logDoneMsg:
Expand Down
2 changes: 1 addition & 1 deletion internal/devtui/model_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import (
func newTestModel() Model {
return Model{
phase: phaseDashboard,
general: NewGeneralModel("local", "http://localhost:8000", "", "", "/tmp/project", nil),
general: NewGeneralModel("local", "http://localhost:8000", "", "", "/tmp/project", nil, nil),
logs: NewLogsModel("/tmp/project", false),
configTab: NewConfigModel(nil),
watchers: make(map[string]*executor.Process),
Expand Down
2 changes: 1 addition & 1 deletion internal/devtui/model_update.go
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,7 @@ func (m Model) startAfterSetupGuide() (tea.Model, tea.Cmd) {
username = m.envConfig.AdminApi.Username
password = m.envConfig.AdminApi.Password
}
m.general = NewGeneralModel(m.executor.Type(), shopURL, username, password, m.projectRoot, m.executor)
m.general = NewGeneralModel(m.executor.Type(), shopURL, username, password, m.projectRoot, m.executor, m.config)
m.configTab = NewConfigModel(m.config)

return m, tea.Batch(m.dockerSpinner.Tick, m.startContainers())
Expand Down
2 changes: 1 addition & 1 deletion internal/devtui/overlay_sales_channel_picker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ func TestSalesChannelPicker_ConfirmEmitsWatcherOpts(t *testing.T) {
func TestModel_SalesChannelPicker_FullRoutingFlow(t *testing.T) {
m := Model{
phase: phaseDashboard,
general: NewGeneralModel("local", "http://localhost:8000", "", "", "/tmp/project", nil),
general: NewGeneralModel("local", "http://localhost:8000", "", "", "/tmp/project", nil, nil),
watchers: make(map[string]*executor.Process),
}

Expand Down
27 changes: 22 additions & 5 deletions internal/devtui/tab_general.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ import (

"github.com/shopware/shopware-cli/internal/executor"
"github.com/shopware/shopware-cli/internal/extension"
"github.com/shopware/shopware-cli/internal/shop"
"github.com/shopware/shopware-cli/internal/tui"
"github.com/shopware/shopware-cli/logging"
)

type GeneralModel struct {
Expand All @@ -23,6 +25,7 @@ type GeneralModel struct {
services []DiscoveredService
projectRoot string
executor executor.Executor
shopCfg *shop.Config
loading bool
err error
width int
Expand Down Expand Up @@ -51,6 +54,7 @@ type watcherStartedMsg struct {
}
type watcherStoppedMsg struct {
name string
err error
}

type knownService struct {
Expand All @@ -72,7 +76,7 @@ var ignoredServices = map[string]bool{
"database": true,
}

func NewGeneralModel(envType, shopURL, username, password, projectRoot string, exec executor.Executor) GeneralModel {
func NewGeneralModel(envType, shopURL, username, password, projectRoot string, exec executor.Executor, shopCfg *shop.Config) GeneralModel {
adminURL := shopURL
if adminURL != "" && !strings.HasSuffix(adminURL, "/") {
adminURL += "/"
Expand All @@ -87,6 +91,7 @@ func NewGeneralModel(envType, shopURL, username, password, projectRoot string, e
password: password,
projectRoot: projectRoot,
executor: exec,
shopCfg: shopCfg,
loading: true,
}
}
Expand Down Expand Up @@ -161,11 +166,17 @@ func (m GeneralModel) View(width, height int) string {
func (m *GeneralModel) startAdminWatch() tea.Cmd {
e := m.executor
projectRoot := m.projectRoot
shopCfg := m.shopCfg

return func() tea.Msg {
watchProcess, err := extension.PrepareAdminWatcher(context.Background(), projectRoot, e)
ctx := logging.DisableLogger(context.Background())
if err := extension.WriteProjectPluginJson(ctx, projectRoot, shopCfg, e); err != nil {
return watcherStoppedMsg{name: watcherAdmin, err: fmt.Errorf("preparing plugins.json: %w", err)}
}

watchProcess, err := extension.PrepareAdminWatcher(ctx, projectRoot, e)
if err != nil {
return watcherStoppedMsg{name: watcherAdmin}
return watcherStoppedMsg{name: watcherAdmin, err: fmt.Errorf("starting admin watcher: %w", err)}
}

return watcherStartedMsg{name: watcherAdmin, process: watchProcess}
Expand All @@ -175,11 +186,17 @@ func (m *GeneralModel) startAdminWatch() tea.Cmd {
func (m *GeneralModel) startStorefrontWatch(opts extension.StorefrontWatcherOptions) tea.Cmd {
e := m.executor
projectRoot := m.projectRoot
shopCfg := m.shopCfg

return func() tea.Msg {
watchProcess, err := extension.PrepareStorefrontWatcher(context.Background(), projectRoot, e, opts)
ctx := logging.DisableLogger(context.Background())
if err := extension.WriteProjectPluginJson(ctx, projectRoot, shopCfg, e); err != nil {
return watcherStoppedMsg{name: watcherStorefront, err: fmt.Errorf("preparing plugins.json: %w", err)}
}

watchProcess, err := extension.PrepareStorefrontWatcher(ctx, projectRoot, e, opts)
if err != nil {
return watcherStoppedMsg{name: watcherStorefront}
return watcherStoppedMsg{name: watcherStorefront, err: fmt.Errorf("starting storefront watcher: %w", err)}
}

return watcherStartedMsg{name: watcherStorefront, process: watchProcess}
Expand Down
12 changes: 6 additions & 6 deletions internal/devtui/tab_general_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
)

func TestNewGeneralModel(t *testing.T) {
m := NewGeneralModel("docker", "http://localhost:8000", "admin", "shopware", "/tmp/project", nil)
m := NewGeneralModel("docker", "http://localhost:8000", "admin", "shopware", "/tmp/project", nil, nil)

assert.Equal(t, "docker", m.envType)
assert.Equal(t, "http://localhost:8000", m.shopURL)
Expand All @@ -19,19 +19,19 @@ func TestNewGeneralModel(t *testing.T) {
}

func TestNewGeneralModel_AdminURLTrailingSlash(t *testing.T) {
m := NewGeneralModel("local", "http://localhost:8000/", "", "", "/tmp/project", nil)
m := NewGeneralModel("local", "http://localhost:8000/", "", "", "/tmp/project", nil, nil)

assert.Equal(t, "http://localhost:8000/admin", m.adminURL)
}

func TestNewGeneralModel_EmptyURL(t *testing.T) {
m := NewGeneralModel("local", "", "", "", "/tmp/project", nil)
m := NewGeneralModel("local", "", "", "", "/tmp/project", nil, nil)

assert.Equal(t, "admin", m.adminURL)
}

func TestServicesLoadedMsg(t *testing.T) {
m := NewGeneralModel("docker", "http://localhost:8000", "", "", "/tmp/project", nil)
m := NewGeneralModel("docker", "http://localhost:8000", "", "", "/tmp/project", nil, nil)

services := []DiscoveredService{
{Name: "Adminer", URL: "http://127.0.0.1:9080", Username: "root", Password: "root"},
Expand All @@ -49,7 +49,7 @@ func TestServicesLoadedMsg(t *testing.T) {
}

func TestServicesLoadedMsg_WithError(t *testing.T) {
m := NewGeneralModel("docker", "http://localhost:8000", "", "", "/tmp/project", nil)
m := NewGeneralModel("docker", "http://localhost:8000", "", "", "/tmp/project", nil, nil)

updated, _ := m.Update(servicesLoadedMsg{err: assert.AnError})
assert.False(t, updated.loading)
Expand Down Expand Up @@ -78,7 +78,7 @@ func TestKnownServices(t *testing.T) {
}

func TestViewShowsCredentials(t *testing.T) {
m := NewGeneralModel("docker", "http://localhost:8000", "", "", "/tmp/project", nil)
m := NewGeneralModel("docker", "http://localhost:8000", "", "", "/tmp/project", nil, nil)
m.loading = false
m.services = []DiscoveredService{
{Name: "Adminer", URL: "http://127.0.0.1:9080", Username: "root", Password: "root"},
Expand Down
8 changes: 8 additions & 0 deletions internal/devtui/tab_logs.go
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,14 @@ func (m *LogsModel) StopStreaming() {
m.stopStreaming()
}

func (m *LogsModel) AppendErrorLine(msg string) {
m.lines = append(m.lines, errorStyle.Render(msg))
m.viewport.SetContent(strings.Join(m.lines, "\n"))
if m.follow {
m.viewport.GotoBottom()
}
}

func (m *LogsModel) ActiveProcessSourceName() string {
if m.active >= 0 && m.active < len(m.sources) {
src := m.sources[m.active]
Expand Down
11 changes: 8 additions & 3 deletions internal/extension/project.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package extension

import (
"bytes"
"context"
"encoding/json"
"fmt"
Expand Down Expand Up @@ -185,11 +186,15 @@ type ConsoleCommandFunc func(ctx context.Context, args ...string) *exec.Cmd
func DumpAndLoadAssetSourcesOfProject(ctx context.Context, project string, shopCfg *shop.Config, consoleCommand ConsoleCommandFunc) ([]asset.Source, error) {
dumpExec := consoleCommand(ctx, "bundle:dump")
dumpExec.Dir = project
dumpExec.Stdin = os.Stdin
dumpExec.Stdout = os.Stdout
dumpExec.Stderr = os.Stderr
// Capture output: bundle:dump's "Dumped plugin configuration." line corrupts the devtui render if inherited.
var dumpOutput bytes.Buffer
dumpExec.Stdout = &dumpOutput
dumpExec.Stderr = &dumpOutput

if err := dumpExec.Run(); err != nil {
if out := strings.TrimSpace(dumpOutput.String()); out != "" {
return nil, fmt.Errorf("could not bundle features: %w: %s", err, out)
}
return nil, fmt.Errorf("could not bundle features: %w", err)
}

Expand Down
Loading