From afab3d2fc8db3d12ce133e8b1cd454b16f27b6e6 Mon Sep 17 00:00:00 2001 From: Louis de Looze Date: Mon, 28 Sep 2026 13:55:59 +0200 Subject: [PATCH 1/3] Replace the store code parameters with an nginx host map (#109) Magento resolved a store by code even for single-store projects, because MageBox always injected MAGE_RUN_CODE and MAGE_RUN_TYPE. A host map now sets them only for domains that name a store code, which also supports store and website mode in one project and removes two optional settings. Closes #107 --- cmd/magebox/domain.go | 19 ++- internal/config/types.go | 32 ++--- internal/lib/templates.go | 1 + internal/nginx/templates/map.conf.tmpl | 17 +++ internal/nginx/templates/vhost.conf.tmpl | 8 +- internal/nginx/vhost.go | 57 +++++++- internal/nginx/vhost_test.go | 157 +++++++++++++++++++++++ lib/templates/nginx/map.conf.tmpl | 17 +++ lib/templates/nginx/vhost.conf.tmpl | 8 +- 9 files changed, 283 insertions(+), 33 deletions(-) create mode 100644 internal/nginx/templates/map.conf.tmpl create mode 100644 lib/templates/nginx/map.conf.tmpl diff --git a/cmd/magebox/domain.go b/cmd/magebox/domain.go index 5f4b687..5da1dd1 100644 --- a/cmd/magebox/domain.go +++ b/cmd/magebox/domain.go @@ -53,12 +53,14 @@ var domainListCmd = &cobra.Command{ var ( domainStoreCode string + domainStoreType string domainRoot string domainSSL bool ) func init() { - domainAddCmd.Flags().StringVar(&domainStoreCode, "store-code", "", "Magento store code (default: \"default\")") + domainAddCmd.Flags().StringVar(&domainStoreCode, "store-code", "", "Magento store/website code for multi-store setup") + domainAddCmd.Flags().StringVar(&domainStoreType, "store-type", "", "Magento run type: \"store\" or \"website\" (default: \"store\")") domainAddCmd.Flags().StringVar(&domainRoot, "root", "", "Document root relative to project (default: \"pub\" for Magento, \"public\" for Laravel)") domainAddCmd.Flags().BoolVar(&domainSSL, "ssl", true, "Enable SSL for the domain") @@ -92,9 +94,10 @@ func runDomainAdd(cmd *cobra.Command, args []string) error { // Create new domain newDomain := config.Domain{ - Host: host, - Root: domainRoot, - MageRunCode: domainStoreCode, + Host: host, + Root: domainRoot, + StoreCode: domainStoreCode, + StoreType: domainStoreType, } // Only set SSL if explicitly changed from default (true) @@ -163,7 +166,9 @@ func runDomainAdd(cmd *cobra.Command, args []string) error { } fmt.Println() - cli.PrintInfo("Domain %s configured with store code: %s", host, newDomain.GetStoreCode()) + if newDomain.StoreCode != "" { + cli.PrintInfo("Domain %s configured with store code: %s (%s)", host, newDomain.StoreCode, newDomain.GetStoreType()) + } return nil } @@ -284,7 +289,9 @@ func runDomainList(cmd *cobra.Command, args []string) error { fmt.Printf(" %s\n", cli.Highlight(d.Host)) fmt.Printf(" URL: %s://%s\n", protocol, d.Host) fmt.Printf(" Root: %s\n", d.GetRoot()) - fmt.Printf(" Store Code: %s\n", d.GetStoreCode()) + if d.StoreCode != "" { + fmt.Printf(" Store Code: %s (%s)\n", d.StoreCode, d.GetStoreType()) + } fmt.Printf(" SSL: %s\n", sslStatus) fmt.Println() } diff --git a/internal/config/types.go b/internal/config/types.go index d0c071a..6284d37 100644 --- a/internal/config/types.go +++ b/internal/config/types.go @@ -174,11 +174,11 @@ func (c *Command) UnmarshalYAML(unmarshal func(interface{}) error) error { // Domain represents a domain configuration type Domain struct { - Host string `yaml:"host"` - Root string `yaml:"root,omitempty"` - SSL *bool `yaml:"ssl,omitempty"` - MageRunCode string `yaml:"mage_run_code,omitempty"` // Magento store/website code for multi-store setup - MageRunType string `yaml:"mage_run_type,omitempty"` // "store" or "website" (default: "store") + Host string `yaml:"host"` + Root string `yaml:"root,omitempty"` + SSL *bool `yaml:"ssl,omitempty"` + StoreCode string `yaml:"store_code,omitempty"` // Magento store/website code for multi-store setup + StoreType string `yaml:"store_type,omitempty"` // "store" or "website" (default: "store") } // Services represents the services configuration @@ -321,20 +321,22 @@ func (d *Domain) IsSSLEnabled() bool { return *d.SSL } -// GetStoreCode returns the Magento store code, defaulting to "default" -func (d *Domain) GetStoreCode() string { - if d.MageRunCode == "" { - return "default" +// GetStoreType returns the Magento run type, defaulting to "store" +func (d *Domain) GetStoreType() string { + if d.StoreType == "" { + return "store" } - return d.MageRunCode + return d.StoreType } -// GetMageRunType returns the Magento run type, defaulting to "store" -func (d *Domain) GetMageRunType() string { - if d.MageRunType == "" { - return "store" +// HasMultiStore returns true if any domain has a store code configured +func (c *Config) HasMultiStore() bool { + for _, d := range c.Domains { + if d.StoreCode != "" { + return true + } } - return d.MageRunType + return false } // Validate checks if the configuration is valid diff --git a/internal/lib/templates.go b/internal/lib/templates.go index e357883..d665cc6 100644 --- a/internal/lib/templates.go +++ b/internal/lib/templates.go @@ -100,6 +100,7 @@ var TemplateNames = map[string][]string{ "vhost.conf.tmpl", "proxy.conf.tmpl", "upstream.conf.tmpl", + "map.conf.tmpl", }, TemplatePHP: { "pool.conf.tmpl", diff --git a/internal/nginx/templates/map.conf.tmpl b/internal/nginx/templates/map.conf.tmpl new file mode 100644 index 0000000..6d1a360 --- /dev/null +++ b/internal/nginx/templates/map.conf.tmpl @@ -0,0 +1,17 @@ +# MageBox multi-store map for {{.ProjectName}} +# Generated from store_code settings in domains configuration +# Do not edit manually - regenerated on magebox start + +map $host $MAGE_RUN_CODE { + hostnames; +{{- range .Domains}}{{if .StoreCode}} + .{{.Host}} {{.StoreCode}}; +{{- end}}{{end}} +} + +map $host $MAGE_RUN_TYPE { + hostnames; +{{- range .Domains}}{{if .StoreCode}} + .{{.Host}} {{.GetStoreType}}; +{{- end}}{{end}} +} diff --git a/internal/nginx/templates/vhost.conf.tmpl b/internal/nginx/templates/vhost.conf.tmpl index d4e494a..b83babf 100644 --- a/internal/nginx/templates/vhost.conf.tmpl +++ b/internal/nginx/templates/vhost.conf.tmpl @@ -106,8 +106,6 @@ server { set $MAGE_ROOT {{.DocumentRoot}}; set $MAGE_MODE developer; - set $MAGE_RUN_CODE {{.StoreCode}}; - set $MAGE_RUN_TYPE {{.MageRunType}}; root $MAGE_ROOT; index index.php; @@ -221,8 +219,10 @@ server { fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; - fastcgi_param MAGE_RUN_CODE $MAGE_RUN_CODE; - fastcgi_param MAGE_RUN_TYPE $MAGE_RUN_TYPE; +{{- if .HasStoreCodes}} + fastcgi_param MAGE_RUN_CODE $MAGE_RUN_CODE if_not_empty; + fastcgi_param MAGE_RUN_TYPE $MAGE_RUN_TYPE if_not_empty; +{{- end}} include fastcgi_params; } diff --git a/internal/nginx/vhost.go b/internal/nginx/vhost.go index 8fd0592..17dc991 100644 --- a/internal/nginx/vhost.go +++ b/internal/nginx/vhost.go @@ -29,12 +29,16 @@ var proxyTemplateEmbed string //go:embed templates/upstream.conf.tmpl var upstreamTemplateEmbed string +//go:embed templates/map.conf.tmpl +var mapTemplateEmbed string + func init() { // Register embedded templates as fallbacks lib.RegisterFallbackTemplate(lib.TemplateNginx, "vhost.conf.tmpl", vhostTemplateEmbed) lib.RegisterFallbackTemplate(lib.TemplateNginx, "vhost-laravel.conf.tmpl", vhostLaravelTemplateEmbed) lib.RegisterFallbackTemplate(lib.TemplateNginx, "proxy.conf.tmpl", proxyTemplateEmbed) lib.RegisterFallbackTemplate(lib.TemplateNginx, "upstream.conf.tmpl", upstreamTemplateEmbed) + lib.RegisterFallbackTemplate(lib.TemplateNginx, "map.conf.tmpl", mapTemplateEmbed) } // Template variables available in vhost.conf.tmpl: @@ -74,13 +78,18 @@ type VhostConfig struct { HTTPSPort int // 443 on Linux, 8443 on macOS (port forwarding) BackendPort int // Backend port for Varnish (always 8080 when Varnish enabled) EnableIPv6 bool // true on Linux to add [::]:port listen directives - StoreCode string // Magento store code for multi-store setup (default: "default") - MageRunType string // Magento run type: "store" or "website" (default: "store") + HasStoreCodes bool // True when any domain has a store_code; enables MAGE_RUN_* fastcgi params AccessLog string // Path to access log file ErrorLog string // Path to error log file CustomNginxDir string // Path to project-level custom nginx snippets directory (if it exists) } +// MapConfig contains data needed to generate the multi-store map config +type MapConfig struct { + ProjectName string + Domains []config.Domain +} + // ProxyConfig contains data needed to generate a proxy vhost type ProxyConfig struct { Name string @@ -132,6 +141,18 @@ func (g *VhostGenerator) Generate(cfg *config.Config, projectPath string) error return fmt.Errorf("failed to generate upstream config: %w", err) } + // Generate or remove the multi-store map config depending on whether any domain has a store_code + hasStoreCodes := cfg.HasMultiStore() + mapFile := filepath.Join(g.vhostsDir, fmt.Sprintf("%s-map.conf", cfg.Name)) + if hasStoreCodes { + mapCfg := MapConfig{ProjectName: cfg.Name, Domains: cfg.Domains} + if err := g.generateMap(mapCfg, mapFile); err != nil { + return fmt.Errorf("failed to generate map config: %w", err) + } + } else { + os.Remove(mapFile) // clean up stale map file if store codes were removed + } + // Determine ports based on platform // macOS uses port forwarding (80->8080, 443->8443), Linux uses standard ports httpPort := 80 @@ -167,8 +188,7 @@ func (g *VhostGenerator) Generate(cfg *config.Config, projectPath string) error HTTPSPort: httpsPort, BackendPort: backendPort, EnableIPv6: enableIPv6, - StoreCode: domain.GetStoreCode(), - MageRunType: domain.GetMageRunType(), + HasStoreCodes: hasStoreCodes, AccessLog: filepath.Join(logsDir, fmt.Sprintf("%s-access.log", sanitizedDomain)), ErrorLog: filepath.Join(logsDir, fmt.Sprintf("%s-error.log", sanitizedDomain)), } @@ -269,6 +289,35 @@ func (g *VhostGenerator) renderVhost(cfg VhostConfig) (string, error) { return buf.String(), nil } +// generateMap generates the multi-store map config file +func (g *VhostGenerator) generateMap(cfg MapConfig, destFile string) error { + content, err := g.renderMap(cfg) + if err != nil { + return err + } + return os.WriteFile(destFile, []byte(content), 0644) +} + +// renderMap renders the map template +func (g *VhostGenerator) renderMap(cfg MapConfig) (string, error) { + tmplContent, err := lib.GetTemplate(lib.TemplateNginx, "map.conf.tmpl") + if err != nil { + return "", err + } + + tmpl, err := template.New("map").Parse(tmplContent) + if err != nil { + return "", err + } + + var buf bytes.Buffer + if err := tmpl.Execute(&buf, cfg); err != nil { + return "", err + } + + return buf.String(), nil +} + // generateUpstream generates the upstream config file for a project func (g *VhostGenerator) generateUpstream(cfg UpstreamConfig) error { content, err := g.renderUpstream(cfg) diff --git a/internal/nginx/vhost_test.go b/internal/nginx/vhost_test.go index 3f53032..66e506e 100644 --- a/internal/nginx/vhost_test.go +++ b/internal/nginx/vhost_test.go @@ -283,6 +283,163 @@ func TestRenderVhost_SSLEnabled_NoIPv6(t *testing.T) { } } +func TestRenderVhost_NoStoreCodes(t *testing.T) { + g, tmpDir := setupTestGenerator(t) + + cfg := VhostConfig{ + ProjectName: "mystore", + Domain: "mystore.test", + DocumentRoot: "/var/www/mystore/pub", + PHPVersion: "8.2", + PHPSocketPath: filepath.Join(tmpDir, ".magebox", "run", "mystore-php8.2.sock"), + HasStoreCodes: false, + HTTPPort: 80, + HTTPSPort: 443, + } + + content, err := g.renderVhost(cfg) + if err != nil { + t.Fatalf("renderVhost failed: %v", err) + } + + if strings.Contains(content, "MAGE_RUN_CODE") { + t.Error("Vhost without store codes should not contain MAGE_RUN_CODE") + } + if strings.Contains(content, "MAGE_RUN_TYPE") { + t.Error("Vhost without store codes should not contain MAGE_RUN_TYPE") + } + if strings.Contains(content, "set $MAGE_RUN") { + t.Error("Vhost without store codes should not contain set $MAGE_RUN_* directives") + } +} + +func TestRenderVhost_WithStoreCodes(t *testing.T) { + g, tmpDir := setupTestGenerator(t) + + cfg := VhostConfig{ + ProjectName: "mystore", + Domain: "mystore.test", + DocumentRoot: "/var/www/mystore/pub", + PHPVersion: "8.2", + PHPSocketPath: filepath.Join(tmpDir, ".magebox", "run", "mystore-php8.2.sock"), + HasStoreCodes: true, + HTTPPort: 80, + HTTPSPort: 443, + } + + content, err := g.renderVhost(cfg) + if err != nil { + t.Fatalf("renderVhost failed: %v", err) + } + + checks := []string{ + "fastcgi_param MAGE_RUN_CODE $MAGE_RUN_CODE if_not_empty", + "fastcgi_param MAGE_RUN_TYPE $MAGE_RUN_TYPE if_not_empty", + } + for _, check := range checks { + if !strings.Contains(content, check) { + t.Errorf("Vhost with store codes should contain %q", check) + } + } + // map-based setup: no set directives in the server block + if strings.Contains(content, "set $MAGE_RUN_CODE") { + t.Error("Vhost should not use set $MAGE_RUN_CODE (handled by map block)") + } +} + +func TestRenderMap(t *testing.T) { + g, _ := setupTestGenerator(t) + + cfg := MapConfig{ + ProjectName: "mystore", + Domains: []config.Domain{ + {Host: "mystore.test", StoreCode: "default"}, + {Host: "de.mystore.test", StoreCode: "german"}, + {Host: "fr.mystore.test", StoreCode: "french", StoreType: "website"}, + {Host: "admin.mystore.test"}, // no store code — should be omitted + }, + } + + content, err := g.renderMap(cfg) + if err != nil { + t.Fatalf("renderMap failed: %v", err) + } + + checks := []string{ + "map $host $MAGE_RUN_CODE", + "map $host $MAGE_RUN_TYPE", + "hostnames;", + ".mystore.test default", + ".de.mystore.test german", + ".fr.mystore.test french", + ".fr.mystore.test website", + } + for _, check := range checks { + if !strings.Contains(content, check) { + t.Errorf("Map content should contain %q\nGot:\n%s", check, content) + } + } + + // Domains without store code should not appear in the map + if strings.Contains(content, "admin.mystore.test") { + t.Error("Domain without store code should not appear in map") + } + // Default store type should be "store" + if !strings.Contains(content, ".mystore.test store") { + t.Error("Domain with no store_type should default to store in MAGE_RUN_TYPE map") + } +} + +func TestGenerate_CreatesMapFileWhenStoreCodesPresent(t *testing.T) { + g, tmpDir := setupTestGenerator(t) + + projectPath := filepath.Join(tmpDir, "projects", "mystore") + cfg := &config.Config{ + Name: "mystore", + Domains: []config.Domain{ + {Host: "mystore.test", Root: "pub", StoreCode: "default"}, + {Host: "de.mystore.test", Root: "pub", StoreCode: "german"}, + }, + PHP: "8.2", + } + + if err := g.Generate(cfg, projectPath); err != nil { + t.Fatalf("Generate failed: %v", err) + } + + mapFile := filepath.Join(g.vhostsDir, "mystore-map.conf") + if _, err := os.Stat(mapFile); os.IsNotExist(err) { + t.Error("Map file should have been created when store codes are present") + } + + content, _ := os.ReadFile(mapFile) + if !strings.Contains(string(content), "map $host $MAGE_RUN_CODE") { + t.Error("Map file should contain MAGE_RUN_CODE map block") + } +} + +func TestGenerate_NoMapFileWithoutStoreCodes(t *testing.T) { + g, tmpDir := setupTestGenerator(t) + + projectPath := filepath.Join(tmpDir, "projects", "mystore") + cfg := &config.Config{ + Name: "mystore", + Domains: []config.Domain{ + {Host: "mystore.test", Root: "pub"}, + }, + PHP: "8.2", + } + + if err := g.Generate(cfg, projectPath); err != nil { + t.Fatalf("Generate failed: %v", err) + } + + mapFile := filepath.Join(g.vhostsDir, "mystore-map.conf") + if _, err := os.Stat(mapFile); !os.IsNotExist(err) { + t.Error("Map file should NOT be created when no store codes are configured") + } +} + func TestRenderVhost_SSLDisabled(t *testing.T) { g, tmpDir := setupTestGenerator(t) diff --git a/lib/templates/nginx/map.conf.tmpl b/lib/templates/nginx/map.conf.tmpl new file mode 100644 index 0000000..6d1a360 --- /dev/null +++ b/lib/templates/nginx/map.conf.tmpl @@ -0,0 +1,17 @@ +# MageBox multi-store map for {{.ProjectName}} +# Generated from store_code settings in domains configuration +# Do not edit manually - regenerated on magebox start + +map $host $MAGE_RUN_CODE { + hostnames; +{{- range .Domains}}{{if .StoreCode}} + .{{.Host}} {{.StoreCode}}; +{{- end}}{{end}} +} + +map $host $MAGE_RUN_TYPE { + hostnames; +{{- range .Domains}}{{if .StoreCode}} + .{{.Host}} {{.GetStoreType}}; +{{- end}}{{end}} +} diff --git a/lib/templates/nginx/vhost.conf.tmpl b/lib/templates/nginx/vhost.conf.tmpl index d4e494a..b83babf 100644 --- a/lib/templates/nginx/vhost.conf.tmpl +++ b/lib/templates/nginx/vhost.conf.tmpl @@ -106,8 +106,6 @@ server { set $MAGE_ROOT {{.DocumentRoot}}; set $MAGE_MODE developer; - set $MAGE_RUN_CODE {{.StoreCode}}; - set $MAGE_RUN_TYPE {{.MageRunType}}; root $MAGE_ROOT; index index.php; @@ -221,8 +219,10 @@ server { fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; - fastcgi_param MAGE_RUN_CODE $MAGE_RUN_CODE; - fastcgi_param MAGE_RUN_TYPE $MAGE_RUN_TYPE; +{{- if .HasStoreCodes}} + fastcgi_param MAGE_RUN_CODE $MAGE_RUN_CODE if_not_empty; + fastcgi_param MAGE_RUN_TYPE $MAGE_RUN_TYPE if_not_empty; +{{- end}} include fastcgi_params; } From 271eb11619cad2ea93bcb5bb86e5ee4f67cabe98 Mon Sep 17 00:00:00 2001 From: Jakub Winkler Date: Mon, 28 Sep 2026 14:08:30 +0200 Subject: [PATCH 2/3] Share one store map, keep the old keys working, validate the type Follow-up to #109, which is otherwise ready. The map was written per project into the shared vhosts directory. nginx accepts one map per variable and includes every file in that directory into the same http block, so a second multistore project would have made it reject the whole configuration with `duplicate "MAGE_RUN_CODE" variable` and serve nothing at all. All projects now share one file, rebuilt from every project when any of them starts, and per-project files from an earlier build are removed. Hosts are matched exactly. With a leading dot in nginx's `hostnames` mode, a deliberately code-less admin.mystore.test inherited the code of mystore.test, which the PR's own test expected not to happen. mage_run_code and mage_run_type keep working with a deprecation warning. The loader ignores unknown keys, so renaming them outright would have let existing multistore projects lose their codes silently and serve the wrong store. A store_type other than store or website now fails validation instead of reaching Magento as a value it does not understand. Docs: the domain properties table, the --store-type flag, the FAQ answer that promised MAGE_RUN_CODE=default, and how the shared map works. Co-Authored-By: Claude Opus 5.5 --- internal/config/loader.go | 4 + internal/config/store_test.go | 65 ++++++++++++ internal/config/types.go | 42 ++++++++ internal/nginx/storemap.go | 139 +++++++++++++++++++++++++ internal/nginx/storemap_test.go | 124 ++++++++++++++++++++++ internal/nginx/templates/map.conf.tmpl | 17 --- internal/nginx/vhost.go | 52 +-------- internal/nginx/vhost_test.go | 87 +++------------- internal/project/lifecycle.go | 8 ++ lib/templates/nginx/map.conf.tmpl | 17 --- vitepress/guide/faq.md | 2 +- vitepress/guide/multi-domain.md | 8 +- vitepress/reference/commands.md | 4 +- vitepress/reference/config-options.md | 9 +- 14 files changed, 416 insertions(+), 162 deletions(-) create mode 100644 internal/config/store_test.go create mode 100644 internal/nginx/storemap.go create mode 100644 internal/nginx/storemap_test.go delete mode 100644 internal/nginx/templates/map.conf.tmpl delete mode 100644 lib/templates/nginx/map.conf.tmpl diff --git a/internal/config/loader.go b/internal/config/loader.go index c0f26df..030fd4b 100644 --- a/internal/config/loader.go +++ b/internal/config/loader.go @@ -99,6 +99,10 @@ func (l *Loader) loadFileWithIncludes(path string, visited map[string]bool) (*Co return nil, &ParseError{Path: path, Err: err} } + // Settings renamed in 2.3.0 are migrated here rather than at each use, so a + // project written for an older release cannot silently lose them. + config.DeprecationWarnings = config.MigrateDeprecatedKeys() + if len(config.IncludeConfig) == 0 { return &config, nil } diff --git a/internal/config/store_test.go b/internal/config/store_test.go new file mode 100644 index 0000000..2391ceb --- /dev/null +++ b/internal/config/store_test.go @@ -0,0 +1,65 @@ +package config + +import "testing" + +// A store type other than store or website silently produces a MAGE_RUN_TYPE +// Magento does not understand, so a typo must fail validation. +func TestConfigValidatesStoreType(t *testing.T) { + base := func(storeType string) *Config { + return &Config{ + Name: "mystore", + PHP: "8.3", + Domains: []Domain{{Host: "mystore.test", StoreCode: "nl", StoreType: storeType}}, + } + } + + tests := []struct { + name string + storeType string + wantError bool + }{ + {name: "unset defaults to store", storeType: "", wantError: false}, + {name: "store", storeType: "store", wantError: false}, + {name: "website", storeType: "website", wantError: false}, + {name: "typo", storeType: "websites", wantError: true}, + {name: "store view", storeType: "store_view", wantError: true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := base(tt.storeType).Validate() + if (err != nil) != tt.wantError { + t.Errorf("Validate() error = %v, wantError %v", err, tt.wantError) + } + }) + } +} + +// Projects written for earlier MageBox releases use mage_run_code and +// mage_run_type. The loader ignores unknown keys, so without a migration those +// projects would quietly lose their store codes and serve the wrong store. +func TestMigrateDeprecatedStoreKeys(t *testing.T) { + cfg := &Config{ + Name: "mystore", + Domains: []Domain{ + {Host: "mystore.test", MageRunCode: "nl", MageRunType: "website"}, + {Host: "de.mystore.test", StoreCode: "de", MageRunCode: "ignored"}, + {Host: "plain.test"}, + }, + } + + warnings := cfg.MigrateDeprecatedKeys() + + if cfg.Domains[0].StoreCode != "nl" || cfg.Domains[0].StoreType != "website" { + t.Errorf("legacy keys were not migrated: %+v", cfg.Domains[0]) + } + if cfg.Domains[1].StoreCode != "de" { + t.Errorf("an explicit store_code must win over the legacy key: %+v", cfg.Domains[1]) + } + if len(warnings) == 0 { + t.Error("migrating a legacy key should report a deprecation warning") + } + if cfg.Domains[2].StoreCode != "" { + t.Errorf("a domain without either key must stay empty: %+v", cfg.Domains[2]) + } +} diff --git a/internal/config/types.go b/internal/config/types.go index 6284d37..3baf902 100644 --- a/internal/config/types.go +++ b/internal/config/types.go @@ -29,6 +29,10 @@ type Config struct { ComposeFile string `yaml:"compose_file,omitempty"` // Path to project-specific docker-compose.yml Sandbox *SandboxConfig `yaml:"sandbox,omitempty"` IncludeConfig []string `yaml:"include_config,omitempty"` // Paths to additional config files or directories to merge + + // DeprecationWarnings are filled by the loader for settings that still work + // under an old name. Not part of the file itself. + DeprecationWarnings []string `yaml:"-"` } // GetType returns the project type, defaulting to "magento" @@ -179,6 +183,13 @@ type Domain struct { SSL *bool `yaml:"ssl,omitempty"` StoreCode string `yaml:"store_code,omitempty"` // Magento store/website code for multi-store setup StoreType string `yaml:"store_type,omitempty"` // "store" or "website" (default: "store") + + // MageRunCode and MageRunType are the names these settings had before 2.3.0. + // They are still read, because the loader ignores unknown keys and a project + // written for an older release would otherwise lose its store code without + // any warning and serve the wrong store. + MageRunCode string `yaml:"mage_run_code,omitempty"` + MageRunType string `yaml:"mage_run_type,omitempty"` } // Services represents the services configuration @@ -355,6 +366,15 @@ func (c *Config) Validate() error { if c.PHP == "" { return &ValidationError{Field: "php", Message: "php version is required"} } + for i, d := range c.Domains { + if !validStoreTypes[d.StoreType] { + return &ValidationError{ + Field: "domains", + Message: fmt.Sprintf("store_type %q is not valid, expected \"store\" or \"website\"", d.StoreType), + Index: i, + } + } + } return nil } @@ -470,3 +490,25 @@ func (s *Services) GetSearchService() *ServiceConfig { } return nil } + +// validStoreTypes are the values Magento understands for MAGE_RUN_TYPE. +var validStoreTypes = map[string]bool{"": true, "store": true, "website": true} + +// MigrateDeprecatedKeys copies settings from the names they had before 2.3.0 and +// returns a warning for each one found, so a project keeps working while its +// owner is told what to rename. +func (c *Config) MigrateDeprecatedKeys() []string { + var warnings []string + for i := range c.Domains { + domain := &c.Domains[i] + if domain.MageRunCode != "" && domain.StoreCode == "" { + domain.StoreCode = domain.MageRunCode + warnings = append(warnings, fmt.Sprintf("domain %s: 'mage_run_code' is deprecated, rename it to 'store_code'", domain.Host)) + } + if domain.MageRunType != "" && domain.StoreType == "" { + domain.StoreType = domain.MageRunType + warnings = append(warnings, fmt.Sprintf("domain %s: 'mage_run_type' is deprecated, rename it to 'store_type'", domain.Host)) + } + } + return warnings +} diff --git a/internal/nginx/storemap.go b/internal/nginx/storemap.go new file mode 100644 index 0000000..0d760d8 --- /dev/null +++ b/internal/nginx/storemap.go @@ -0,0 +1,139 @@ +package nginx + +import ( + "bytes" + "fmt" + "os" + "path/filepath" + "sort" + "strings" + "text/template" + + "qoliber/magebox/internal/config" +) + +// StoreMapFile holds the host to store mapping for every project. +// +// nginx accepts one map per variable, and every file in the vhosts directory is +// included into the same http block. A map per project therefore makes nginx +// reject the whole configuration with `duplicate "MAGE_RUN_CODE" variable`, +// taking down every project on the machine, so all projects share this file. +// The numeric prefix keeps it ahead of the vhosts that read the variables. +const StoreMapFile = "000-magebox-store-map.conf" + +// storeMapTemplate renders the shared map. Hosts are matched exactly: nginx's +// `hostnames` mode with a leading dot would also match every subdomain, so a +// deliberately code-less admin host would inherit its parent's store code. +const storeMapTemplate = `# MageBox store map for every project with store codes +# Do not edit manually - regenerated on magebox start + +map $host $MAGE_RUN_CODE { + default ""; +{{- range .}} + {{.Host}} {{.Code}}; +{{- end}} +} + +map $host $MAGE_RUN_TYPE { + default ""; +{{- range .}} + {{.Host}} {{.Type}}; +{{- end}} +} +` + +// StoreMapEntry is one host in the shared map. +type StoreMapEntry struct { + Host string + Code string + Type string +} + +// StoreMapEntries collects the mapping from every project, sorted by host so +// the generated file does not churn between runs. +func StoreMapEntries(projects []*config.Config) []StoreMapEntry { + var entries []StoreMapEntry + for _, project := range projects { + if project == nil { + continue + } + for _, domain := range project.Domains { + if domain.StoreCode == "" { + continue + } + entries = append(entries, StoreMapEntry{ + Host: domain.Host, + Code: domain.StoreCode, + Type: domain.GetStoreType(), + }) + } + } + sort.Slice(entries, func(i, j int) bool { return entries[i].Host < entries[j].Host }) + return entries +} + +// RenderStoreMap renders the shared map file. +func RenderStoreMap(entries []StoreMapEntry) (string, error) { + tmpl, err := template.New("storemap").Parse(storeMapTemplate) + if err != nil { + return "", fmt.Errorf("failed to parse the store map template: %w", err) + } + var buf bytes.Buffer + if err := tmpl.Execute(&buf, entries); err != nil { + return "", fmt.Errorf("failed to render the store map: %w", err) + } + return buf.String(), nil +} + +// WriteStoreMap installs the shared map, or removes it when no project uses +// store codes, and clears the per-project map files older releases wrote. +func WriteStoreMap(vhostsDir string, entries []StoreMapEntry) error { + if err := removeLegacyStoreMaps(vhostsDir); err != nil { + return err + } + + path := filepath.Join(vhostsDir, StoreMapFile) + if len(entries) == 0 { + // No vhost references the variables, and an empty map left behind would + // only be one more file to explain. + if err := os.Remove(path); err != nil && !os.IsNotExist(err) { + return fmt.Errorf("failed to remove %s: %w", path, err) + } + return nil + } + + content, err := RenderStoreMap(entries) + if err != nil { + return err + } + if err := os.MkdirAll(vhostsDir, 0755); err != nil { + return fmt.Errorf("failed to create %s: %w", vhostsDir, err) + } + return os.WriteFile(path, []byte(content), 0644) +} + +// removeLegacyStoreMaps deletes the -map.conf files written before the +// map was shared. Left in place they reintroduce the duplicate variable. +func removeLegacyStoreMaps(vhostsDir string) error { + matches, err := filepath.Glob(filepath.Join(vhostsDir, "*-map.conf")) + if err != nil { + return err + } + for _, match := range matches { + if strings.HasSuffix(match, StoreMapFile) { + continue + } + if err := os.Remove(match); err != nil && !os.IsNotExist(err) { + return fmt.Errorf("failed to remove the legacy map %s: %w", match, err) + } + } + return nil +} + +// EnsureStoreMap rebuilds the shared map from every project MageBox knows about. +// +// It takes all projects rather than one, because a single file has to describe +// them all: nginx permits one map per variable. +func (g *VhostGenerator) EnsureStoreMap(projects []*config.Config) error { + return WriteStoreMap(g.vhostsDir, StoreMapEntries(projects)) +} diff --git a/internal/nginx/storemap_test.go b/internal/nginx/storemap_test.go new file mode 100644 index 0000000..b57367b --- /dev/null +++ b/internal/nginx/storemap_test.go @@ -0,0 +1,124 @@ +package nginx + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "qoliber/magebox/internal/config" +) + +func project(name string, domains ...config.Domain) *config.Config { + return &config.Config{Name: name, Domains: domains} +} + +// nginx allows one map per variable. A map block per project puts several in the +// shared vhosts directory, which nginx rejects with `duplicate "MAGE_RUN_CODE" +// variable`, and then no project on the machine serves at all. +func TestRenderStoreMapHasOneMapPerVariableForManyProjects(t *testing.T) { + projects := []*config.Config{ + project("shop", config.Domain{Host: "shop.test", StoreCode: "nl"}, config.Domain{Host: "de.shop.test", StoreCode: "de", StoreType: "website"}), + project("blog", config.Domain{Host: "blog.test", StoreCode: "blog"}), + project("plain", config.Domain{Host: "plain.test"}), + } + + content, err := RenderStoreMap(StoreMapEntries(projects)) + if err != nil { + t.Fatalf("RenderStoreMap failed: %v", err) + } + + if got := strings.Count(content, "map $host $MAGE_RUN_CODE"); got != 1 { + t.Errorf("found %d MAGE_RUN_CODE maps, want exactly 1:\n%s", got, content) + } + if got := strings.Count(content, "map $host $MAGE_RUN_TYPE"); got != 1 { + t.Errorf("found %d MAGE_RUN_TYPE maps, want exactly 1:\n%s", got, content) + } + for _, want := range []string{"shop.test", "de.shop.test", "blog.test"} { + if !strings.Contains(content, want) { + t.Errorf("map is missing host %q:\n%s", want, content) + } + } + if strings.Contains(content, "plain.test") { + t.Errorf("a domain without a store code must not appear:\n%s", content) + } +} + +// A leading dot with `hostnames` matches every subdomain, so a deliberately +// code-less admin subdomain would inherit its parent's store code. +func TestRenderStoreMapMatchesExactHostsOnly(t *testing.T) { + content, err := RenderStoreMap(StoreMapEntries([]*config.Config{ + project("shop", config.Domain{Host: "shop.test", StoreCode: "nl"}), + })) + if err != nil { + t.Fatalf("RenderStoreMap failed: %v", err) + } + + if strings.Contains(content, "hostnames") { + t.Errorf("the map must not use wildcard hostname matching:\n%s", content) + } + if strings.Contains(content, ".shop.test") { + t.Errorf("hosts must be exact, not dot-prefixed:\n%s", content) + } +} + +func TestStoreMapEntries(t *testing.T) { + entries := StoreMapEntries([]*config.Config{ + project("b", config.Domain{Host: "b.test", StoreCode: "b"}), + project("a", config.Domain{Host: "a.test", StoreCode: "a", StoreType: "website"}, config.Domain{Host: "nocode.test"}), + }) + + if len(entries) != 2 { + t.Fatalf("got %d entries, want 2: %+v", len(entries), entries) + } + // Sorted, so the generated file does not churn between runs. + if entries[0].Host != "a.test" || entries[1].Host != "b.test" { + t.Errorf("entries are not sorted by host: %+v", entries) + } + if entries[0].Type != "website" { + t.Errorf("explicit store type lost: %+v", entries[0]) + } + if entries[1].Type != "store" { + t.Errorf("missing store type should default to store: %+v", entries[1]) + } +} + +// Upgrading from the per-project map must not leave those files behind, or the +// duplicate variable they cause keeps nginx broken. +func TestEnsureStoreMapRemovesLegacyPerProjectFiles(t *testing.T) { + dir := t.TempDir() + legacy := filepath.Join(dir, "shop-map.conf") + if err := os.WriteFile(legacy, []byte("map $host $MAGE_RUN_CODE {}\n"), 0644); err != nil { + t.Fatal(err) + } + + if err := WriteStoreMap(dir, StoreMapEntries([]*config.Config{ + project("shop", config.Domain{Host: "shop.test", StoreCode: "nl"}), + })); err != nil { + t.Fatalf("WriteStoreMap failed: %v", err) + } + + if _, err := os.Stat(legacy); !os.IsNotExist(err) { + t.Error("the legacy per-project map file was not removed") + } + if _, err := os.Stat(filepath.Join(dir, StoreMapFile)); err != nil { + t.Errorf("the shared map file was not written: %v", err) + } +} + +// With no store codes anywhere the file must go, so no vhost references a +// variable nginx does not know. +func TestWriteStoreMapRemovesFileWhenNoStoreCodes(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, StoreMapFile) + if err := os.WriteFile(path, []byte("map $host $MAGE_RUN_CODE {}\n"), 0644); err != nil { + t.Fatal(err) + } + + if err := WriteStoreMap(dir, StoreMapEntries([]*config.Config{project("plain", config.Domain{Host: "plain.test"})})); err != nil { + t.Fatalf("WriteStoreMap failed: %v", err) + } + if _, err := os.Stat(path); !os.IsNotExist(err) { + t.Error("the shared map file should be removed when nothing uses store codes") + } +} diff --git a/internal/nginx/templates/map.conf.tmpl b/internal/nginx/templates/map.conf.tmpl deleted file mode 100644 index 6d1a360..0000000 --- a/internal/nginx/templates/map.conf.tmpl +++ /dev/null @@ -1,17 +0,0 @@ -# MageBox multi-store map for {{.ProjectName}} -# Generated from store_code settings in domains configuration -# Do not edit manually - regenerated on magebox start - -map $host $MAGE_RUN_CODE { - hostnames; -{{- range .Domains}}{{if .StoreCode}} - .{{.Host}} {{.StoreCode}}; -{{- end}}{{end}} -} - -map $host $MAGE_RUN_TYPE { - hostnames; -{{- range .Domains}}{{if .StoreCode}} - .{{.Host}} {{.GetStoreType}}; -{{- end}}{{end}} -} diff --git a/internal/nginx/vhost.go b/internal/nginx/vhost.go index 17dc991..02e6839 100644 --- a/internal/nginx/vhost.go +++ b/internal/nginx/vhost.go @@ -29,16 +29,12 @@ var proxyTemplateEmbed string //go:embed templates/upstream.conf.tmpl var upstreamTemplateEmbed string -//go:embed templates/map.conf.tmpl -var mapTemplateEmbed string - func init() { // Register embedded templates as fallbacks lib.RegisterFallbackTemplate(lib.TemplateNginx, "vhost.conf.tmpl", vhostTemplateEmbed) lib.RegisterFallbackTemplate(lib.TemplateNginx, "vhost-laravel.conf.tmpl", vhostLaravelTemplateEmbed) lib.RegisterFallbackTemplate(lib.TemplateNginx, "proxy.conf.tmpl", proxyTemplateEmbed) lib.RegisterFallbackTemplate(lib.TemplateNginx, "upstream.conf.tmpl", upstreamTemplateEmbed) - lib.RegisterFallbackTemplate(lib.TemplateNginx, "map.conf.tmpl", mapTemplateEmbed) } // Template variables available in vhost.conf.tmpl: @@ -84,12 +80,6 @@ type VhostConfig struct { CustomNginxDir string // Path to project-level custom nginx snippets directory (if it exists) } -// MapConfig contains data needed to generate the multi-store map config -type MapConfig struct { - ProjectName string - Domains []config.Domain -} - // ProxyConfig contains data needed to generate a proxy vhost type ProxyConfig struct { Name string @@ -141,17 +131,10 @@ func (g *VhostGenerator) Generate(cfg *config.Config, projectPath string) error return fmt.Errorf("failed to generate upstream config: %w", err) } - // Generate or remove the multi-store map config depending on whether any domain has a store_code + // The host map is shared by every project and written by the caller, which + // is the only place that knows all of them. One map per project would give + // nginx a duplicate variable and stop it serving anything. hasStoreCodes := cfg.HasMultiStore() - mapFile := filepath.Join(g.vhostsDir, fmt.Sprintf("%s-map.conf", cfg.Name)) - if hasStoreCodes { - mapCfg := MapConfig{ProjectName: cfg.Name, Domains: cfg.Domains} - if err := g.generateMap(mapCfg, mapFile); err != nil { - return fmt.Errorf("failed to generate map config: %w", err) - } - } else { - os.Remove(mapFile) // clean up stale map file if store codes were removed - } // Determine ports based on platform // macOS uses port forwarding (80->8080, 443->8443), Linux uses standard ports @@ -289,35 +272,6 @@ func (g *VhostGenerator) renderVhost(cfg VhostConfig) (string, error) { return buf.String(), nil } -// generateMap generates the multi-store map config file -func (g *VhostGenerator) generateMap(cfg MapConfig, destFile string) error { - content, err := g.renderMap(cfg) - if err != nil { - return err - } - return os.WriteFile(destFile, []byte(content), 0644) -} - -// renderMap renders the map template -func (g *VhostGenerator) renderMap(cfg MapConfig) (string, error) { - tmplContent, err := lib.GetTemplate(lib.TemplateNginx, "map.conf.tmpl") - if err != nil { - return "", err - } - - tmpl, err := template.New("map").Parse(tmplContent) - if err != nil { - return "", err - } - - var buf bytes.Buffer - if err := tmpl.Execute(&buf, cfg); err != nil { - return "", err - } - - return buf.String(), nil -} - // generateUpstream generates the upstream config file for a project func (g *VhostGenerator) generateUpstream(cfg UpstreamConfig) error { content, err := g.renderUpstream(cfg) diff --git a/internal/nginx/vhost_test.go b/internal/nginx/vhost_test.go index 66e506e..3333252 100644 --- a/internal/nginx/vhost_test.go +++ b/internal/nginx/vhost_test.go @@ -347,53 +347,12 @@ func TestRenderVhost_WithStoreCodes(t *testing.T) { } } -func TestRenderMap(t *testing.T) { - g, _ := setupTestGenerator(t) - - cfg := MapConfig{ - ProjectName: "mystore", - Domains: []config.Domain{ - {Host: "mystore.test", StoreCode: "default"}, - {Host: "de.mystore.test", StoreCode: "german"}, - {Host: "fr.mystore.test", StoreCode: "french", StoreType: "website"}, - {Host: "admin.mystore.test"}, // no store code — should be omitted - }, - } - - content, err := g.renderMap(cfg) - if err != nil { - t.Fatalf("renderMap failed: %v", err) - } - - checks := []string{ - "map $host $MAGE_RUN_CODE", - "map $host $MAGE_RUN_TYPE", - "hostnames;", - ".mystore.test default", - ".de.mystore.test german", - ".fr.mystore.test french", - ".fr.mystore.test website", - } - for _, check := range checks { - if !strings.Contains(content, check) { - t.Errorf("Map content should contain %q\nGot:\n%s", check, content) - } - } - - // Domains without store code should not appear in the map - if strings.Contains(content, "admin.mystore.test") { - t.Error("Domain without store code should not appear in map") - } - // Default store type should be "store" - if !strings.Contains(content, ".mystore.test store") { - t.Error("Domain with no store_type should default to store in MAGE_RUN_TYPE map") - } -} - -func TestGenerate_CreatesMapFileWhenStoreCodesPresent(t *testing.T) { +// The host map is shared by every project (see storemap_test.go). Generate must +// not write a per-project one, because nginx rejects a duplicate map variable +// and then serves no project at all. +func TestGenerateWritesNoPerProjectMap(t *testing.T) { g, tmpDir := setupTestGenerator(t) - projectPath := filepath.Join(tmpDir, "projects", "mystore") cfg := &config.Config{ Name: "mystore", Domains: []config.Domain{ @@ -403,40 +362,18 @@ func TestGenerate_CreatesMapFileWhenStoreCodesPresent(t *testing.T) { PHP: "8.2", } - if err := g.Generate(cfg, projectPath); err != nil { + if err := g.Generate(cfg, filepath.Join(tmpDir, "projects", "mystore")); err != nil { t.Fatalf("Generate failed: %v", err) } - mapFile := filepath.Join(g.vhostsDir, "mystore-map.conf") - if _, err := os.Stat(mapFile); os.IsNotExist(err) { - t.Error("Map file should have been created when store codes are present") - } - - content, _ := os.ReadFile(mapFile) - if !strings.Contains(string(content), "map $host $MAGE_RUN_CODE") { - t.Error("Map file should contain MAGE_RUN_CODE map block") - } -} - -func TestGenerate_NoMapFileWithoutStoreCodes(t *testing.T) { - g, tmpDir := setupTestGenerator(t) - - projectPath := filepath.Join(tmpDir, "projects", "mystore") - cfg := &config.Config{ - Name: "mystore", - Domains: []config.Domain{ - {Host: "mystore.test", Root: "pub"}, - }, - PHP: "8.2", - } - - if err := g.Generate(cfg, projectPath); err != nil { - t.Fatalf("Generate failed: %v", err) + matches, err := filepath.Glob(filepath.Join(g.vhostsDir, "*-map.conf")) + if err != nil { + t.Fatal(err) } - - mapFile := filepath.Join(g.vhostsDir, "mystore-map.conf") - if _, err := os.Stat(mapFile); !os.IsNotExist(err) { - t.Error("Map file should NOT be created when no store codes are configured") + for _, match := range matches { + if filepath.Base(match) != StoreMapFile { + t.Errorf("Generate wrote a per-project map file: %s", match) + } } } diff --git a/internal/project/lifecycle.go b/internal/project/lifecycle.go index 33ba775..2c7a336 100644 --- a/internal/project/lifecycle.go +++ b/internal/project/lifecycle.go @@ -167,6 +167,14 @@ func (m *Manager) Start(projectPath string) (*StartResult, error) { result.Errors = append(result.Errors, fmt.Errorf("nginx vhost: %w", err)) } + // The host to store map is shared by every project, so it is rebuilt from + // all of them: nginx accepts only one map per variable. + if err := m.vhostGenerator.EnsureStoreMap(m.collectAllProjectConfigs(cfg)); err != nil { + result.Errors = append(result.Errors, fmt.Errorf("store map: %w", err)) + } + + result.Warnings = append(result.Warnings, cfg.DeprecationWarnings...) + // Reload Nginx to pick up new vhost nginxController := nginx.NewController(m.platform) if err := nginxController.Reload(); err != nil { diff --git a/lib/templates/nginx/map.conf.tmpl b/lib/templates/nginx/map.conf.tmpl deleted file mode 100644 index 6d1a360..0000000 --- a/lib/templates/nginx/map.conf.tmpl +++ /dev/null @@ -1,17 +0,0 @@ -# MageBox multi-store map for {{.ProjectName}} -# Generated from store_code settings in domains configuration -# Do not edit manually - regenerated on magebox start - -map $host $MAGE_RUN_CODE { - hostnames; -{{- range .Domains}}{{if .StoreCode}} - .{{.Host}} {{.StoreCode}}; -{{- end}}{{end}} -} - -map $host $MAGE_RUN_TYPE { - hostnames; -{{- range .Domains}}{{if .StoreCode}} - .{{.Host}} {{.GetStoreType}}; -{{- end}}{{end}} -} diff --git a/vitepress/guide/faq.md b/vitepress/guide/faq.md index 5abba30..6859ea7 100644 --- a/vitepress/guide/faq.md +++ b/vitepress/guide/faq.md @@ -296,7 +296,7 @@ Magento\Framework\Exception\NoSuchEntityException: The store that was requested Verify the store and try again. ``` -This happens because MageBox sets `MAGE_RUN_CODE` to `default` when no store code is configured for a domain. If your Magento installation's default store code is not `default`, you need to specify the correct store code: +Up to 2.2 MageBox set `MAGE_RUN_CODE` to `default` for every domain, so Magento looked for a store with that exact code. From 2.3.0 it sends nothing unless the domain names a `store_code`, and Magento resolves the store itself. On an older release, or when you do want a specific store per domain, set the code: ```bash magebox domain add mystore.test --store-code=my_store_code diff --git a/vitepress/guide/multi-domain.md b/vitepress/guide/multi-domain.md index 0d69f5f..bdf99cf 100644 --- a/vitepress/guide/multi-domain.md +++ b/vitepress/guide/multi-domain.md @@ -91,9 +91,15 @@ In Magento Admin: ## Store Code Mapping +### How it works + +Hosts that name a `store_code` go into `~/.magebox/nginx/vhosts/000-magebox-store-map.conf`, one file for every project, because nginx accepts only one map per variable. Matching is exact, so `admin.mystore.test` does not inherit the code of `mystore.test`. Domains without a code send nothing and let Magento resolve the store. + +Use `store_type: website` for a code that names a website rather than a store view. Both can appear in one project. + ### Using store_code in Configuration (Recommended) -MageBox supports `store_code` directly in domain configuration. This sets `MAGE_RUN_CODE` in nginx: +MageBox supports `store_code` directly in domain configuration. Every domain that names one is added to a single nginx map shared by all projects, which sets `MAGE_RUN_CODE` and `MAGE_RUN_TYPE` for exactly that host: ```yaml name: mystore diff --git a/vitepress/reference/commands.md b/vitepress/reference/commands.md index ff4efe1..c9716fa 100644 --- a/vitepress/reference/commands.md +++ b/vitepress/reference/commands.md @@ -1410,11 +1410,13 @@ Add a domain to the project. ```bash magebox domain add store.test magebox domain add de.store.test --store-code=german +magebox domain add fr.store.test --store-code=french --store-type=website magebox domain add api.store.test --root=pub --ssl=false ``` **Options:** -- `--store-code` - Magento store code (sets `MAGE_RUN_CODE`) +- `--store-code` - Magento store or website code (sets `MAGE_RUN_CODE` for this host) +- `--store-type` - What the code refers to: `store` (default) or `website` (sets `MAGE_RUN_TYPE`) - `--root` - Document root relative to project (default: `pub`) - `--ssl` - Enable SSL for the domain (default: `true`) diff --git a/vitepress/reference/config-options.md b/vitepress/reference/config-options.md index 5dda6cd..3731af4 100644 --- a/vitepress/reference/config-options.md +++ b/vitepress/reference/config-options.md @@ -57,7 +57,14 @@ domains: | `host` | string | required | Domain name | | `root` | string | `pub` | Document root relative to project | | `ssl` | boolean | `true` | Enable HTTPS | -| `store_code` | string | `default` | Magento store code (sets `MAGE_RUN_CODE`) | +| `store_code` | string | *(none)* | Magento store or website code. Sets `MAGE_RUN_CODE` for this host only when given | +| `store_type` | string | `store` | What the code refers to: `store` or `website`. Sets `MAGE_RUN_TYPE` | + +Leave `store_code` out for a single-store project. MageBox then sends no `MAGE_RUN_CODE` at all and Magento resolves the store itself, which is what a project whose default store code is not `default` needs. + +::: tip Renamed in 2.3.0 +`mage_run_code` and `mage_run_type` are the former names. They still work and print a deprecation warning on `magebox start`; rename them to `store_code` and `store_type`. +::: --- From 3363d6a9ffe590fa98793a89d863e2d0458e4132 Mon Sep 17 00:00:00 2001 From: Jakub Winkler Date: Mon, 28 Sep 2026 14:08:30 +0200 Subject: [PATCH 3/3] Release v2.2.0 Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 17 +++++++++++++++++ VERSION | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 425f4e3..a78a728 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,23 @@ All notable changes to MageBox will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.2.0] - 2026-09-28 + +### Changed + +- **`MAGE_RUN_CODE` Is Only Sent When a Domain Names a Store** - MageBox injected `MAGE_RUN_CODE` and `MAGE_RUN_TYPE` into every vhost, so Magento looked for a store with that exact code even in single-store projects whose default code is not `default`, instead of resolving the store itself. A domain now contributes those variables only when it sets `store_code`, and one project can mix store and website codes. Thanks to Louis de Looze. ([#109](https://github.com/qoliber/magebox/pull/109), [#107](https://github.com/qoliber/magebox/issues/107)) +- **`mage_run_code` and `mage_run_type` Renamed to `store_code` and `store_type`** - The old names still work and print a deprecation warning on `magebox start`, because the config loader ignores unknown keys and a project written for an older release would otherwise lose its store codes silently and serve the wrong store. + +### Added + +- **`--store-type` on `magebox domain add`** - Declares whether a code names a store view or a website. + +### Fixed + +- **A Second Multistore Project Would Have Stopped nginx Entirely** - The store map was written per project into the shared vhosts directory, and nginx rejects a second `map` for the same variable with `duplicate "MAGE_RUN_CODE" variable`, which stops it serving every project on the machine. All projects now share one map file, rebuilt from every project on `magebox start`, and the per-project files an earlier build left behind are removed. +- **Store Codes Leaked into Subdomains** - The map matched with a leading dot in nginx's `hostnames` mode, so a deliberately code-less `admin.mystore.test` inherited the code of `mystore.test`. Hosts are now matched exactly. +- **An Invalid `store_type` Passed Silently** - Anything other than `store` or `website` was sent to Magento as `MAGE_RUN_TYPE`. A typo now fails validation. + ## [2.1.2] - 2026-09-24 ### Fixed diff --git a/VERSION b/VERSION index eca07e4..ccbccc3 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.1.2 +2.2.0