diff --git a/cmd/internal/migrations/lists.go b/cmd/internal/migrations/lists.go index ef36344..0122103 100644 --- a/cmd/internal/migrations/lists.go +++ b/cmd/internal/migrations/lists.go @@ -73,6 +73,7 @@ var Migrations = []Migration{ v3migrations.MigrateSessionExtractor, v3migrations.MigrateSessionStore, v3migrations.MigrateStorageVersions, + v3migrations.MigrateTemplateVersions, v3migrations.MigrateSessionRelease, v3migrations.MigrateKeyAuthConfig, v3migrations.MigrateJWTExtractor, diff --git a/cmd/internal/migrations/v3/swagger_packages.go b/cmd/internal/migrations/v3/swagger_packages.go index 7329164..3e0848a 100644 --- a/cmd/internal/migrations/v3/swagger_packages.go +++ b/cmd/internal/migrations/v3/swagger_packages.go @@ -25,6 +25,8 @@ const ( fiberSwaggerOld = "github.com/gofiber/swagger" fiberSwaggerNew = "github.com/gofiber/contrib/v3/swaggo" goModVersionPattern = `v[a-zA-Z0-9.+-]+` + vendorDirName = "vendor" + goModFileName = "go.mod" ) func MigrateSwaggerPackages(cmd *cobra.Command, cwd string, _, _ *semver.Version) error { @@ -61,12 +63,12 @@ func migrateSwaggerModules(cwd string) (bool, error) { return walkErr } if d.IsDir() { - if d.Name() == "vendor" { + if d.Name() == vendorDirName { return filepath.SkipDir } return nil } - if d.Name() != "go.mod" { + if d.Name() != goModFileName { return nil } diff --git a/cmd/internal/migrations/v3/template_versions.go b/cmd/internal/migrations/v3/template_versions.go new file mode 100644 index 0000000..d9e3424 --- /dev/null +++ b/cmd/internal/migrations/v3/template_versions.go @@ -0,0 +1,157 @@ +package v3 + +import ( + "fmt" + "io/fs" + "os" + "path/filepath" + "regexp" + "strings" + + semver "github.com/Masterminds/semver/v3" + "github.com/spf13/cobra" + + "github.com/gofiber/cli/cmd/internal" +) + +var reTemplateImport = regexp.MustCompile(`"github\.com/gofiber/template/([a-zA-Z0-9_-]+)(?:/v(\d+))?([^\"]*)"`) + +type templateTarget struct { + modulePath string + version string +} + +type templateGoModPatterns struct { + require *regexp.Regexp + replace *regexp.Regexp +} + +// templateTargets maps template packages to their minimum module path and version +// required for Fiber v3 core compatibility. Source: +// https://github.com/gofiber/template/pull/437 +var templateTargets = map[string]templateTarget{ + "ace": {modulePath: "github.com/gofiber/template/ace/v3", version: "v3.0.0"}, + "amber": {modulePath: "github.com/gofiber/template/amber/v3", version: "v3.0.0"}, + "django": {modulePath: "github.com/gofiber/template/django/v4", version: "v4.0.0"}, + "handlebars": {modulePath: "github.com/gofiber/template/handlebars/v3", version: "v3.0.0"}, + "html": {modulePath: "github.com/gofiber/template/html/v3", version: "v3.0.0"}, + "jet": {modulePath: "github.com/gofiber/template/jet/v3", version: "v3.0.0"}, + "mustache": {modulePath: "github.com/gofiber/template/mustache/v3", version: "v3.0.0"}, + "pug": {modulePath: "github.com/gofiber/template/pug/v3", version: "v3.0.0"}, + "slim": {modulePath: "github.com/gofiber/template/slim/v3", version: "v3.0.0"}, +} + +// MigrateTemplateVersions updates template package imports and go.mod entries +// to the minimum versions required for Fiber v3 core compatibility. +func MigrateTemplateVersions(cmd *cobra.Command, cwd string, _, _ *semver.Version) error { + changedImports, err := internal.ChangeFileContent(cwd, func(content string) string { + return reTemplateImport.ReplaceAllStringFunc(content, func(match string) string { + sub := reTemplateImport.FindStringSubmatch(match) + if len(sub) != 4 { + return match + } + + target, ok := templateTargets[sub[1]] + if !ok { + return match + } + + return fmt.Sprintf(`"%s%s"`, target.modulePath, sub[3]) + }) + }) + if err != nil { + return fmt.Errorf("failed to migrate template imports: %w", err) + } + + modChanged, err := migrateTemplateModules(cwd) + if err != nil { + return err + } + + if !changedImports && !modChanged { + return nil + } + + cmd.Println("Migrated template package versions") + return nil +} + +func migrateTemplateModules(cwd string) (bool, error) { + modChanged := false + patterns := compileTemplateGoModPatterns() + + walkErr := filepath.WalkDir(cwd, func(path string, d fs.DirEntry, walkErr error) error { + if walkErr != nil { + return walkErr + } + if d.IsDir() { + if d.Name() == vendorDirName { + return filepath.SkipDir + } + return nil + } + if d.Name() != goModFileName { + return nil + } + + info, err := d.Info() + if err != nil { + return fmt.Errorf("stat %s: %w", path, err) + } + + b, err := os.ReadFile(path) // #nosec G304 -- reading module file + if err != nil { + return fmt.Errorf("read %s: %w", path, err) + } + content := string(b) + updated := content + + for pkg, target := range templateTargets { + updated = updateTemplateGoModModule(updated, target.modulePath, target.version, patterns[pkg]) + } + + if updated == content { + return nil + } + + if err := os.WriteFile(path, []byte(updated), info.Mode().Perm()); err != nil { + return fmt.Errorf("write %s: %w", path, err) + } + modChanged = true + return nil + }) + if walkErr != nil { + return false, fmt.Errorf("failed to migrate template modules: %w", walkErr) + } + + return modChanged, nil +} + +func compileTemplateGoModPatterns() map[string]templateGoModPatterns { + patterns := make(map[string]templateGoModPatterns, len(templateTargets)) + for pkg := range templateTargets { + rePath := fmt.Sprintf(`github\.com/gofiber/template/%s(?:/v\d+)?`, regexp.QuoteMeta(pkg)) + patterns[pkg] = templateGoModPatterns{ + require: regexp.MustCompile(fmt.Sprintf(`(?m)^(\s*(?:require\s+)?)%s\s+%s`, rePath, goModVersionPattern)), + replace: regexp.MustCompile(fmt.Sprintf(`(?m)^(\s*replace\s+)%s(\s+%s)?(\s+=>\s+)`, rePath, goModVersionPattern)), + } + } + return patterns +} + +func updateTemplateGoModModule(content, newPath, version string, patterns templateGoModPatterns) string { + content = patterns.require.ReplaceAllString(content, fmt.Sprintf(`${1}%s %s`, newPath, version)) + content = patterns.replace.ReplaceAllStringFunc(content, func(s string) string { + sub := patterns.replace.FindStringSubmatch(s) + if len(sub) != 4 { + return s + } + + if strings.TrimSpace(sub[2]) == "" { + return fmt.Sprintf("%s%s%s", sub[1], newPath, sub[3]) + } + return fmt.Sprintf("%s%s %s%s", sub[1], newPath, version, sub[3]) + }) + + return content +} diff --git a/cmd/internal/migrations/v3/template_versions_test.go b/cmd/internal/migrations/v3/template_versions_test.go new file mode 100644 index 0000000..9626659 --- /dev/null +++ b/cmd/internal/migrations/v3/template_versions_test.go @@ -0,0 +1,119 @@ +package v3_test + +import ( + "bytes" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/gofiber/cli/cmd/internal/migrations/v3" +) + +func Test_MigrateTemplateVersions(t *testing.T) { + dir := t.TempDir() + + file := writeTempFile(t, dir, `package main +import ( + amber "github.com/gofiber/template/amber" + django "github.com/gofiber/template/django/v3" + html "github.com/gofiber/template/html/v2" +) + +var ( + _ = html.New + _ = django.New + _ = amber.New +) +`) + + modContent := `module example + +go 1.22 + +require ( + github.com/gofiber/template/html/v2 v2.0.0 + github.com/gofiber/template/django/v3 v3.0.1 + github.com/gofiber/template/amber v1.8.3 +) + +replace github.com/gofiber/template/html/v2 => ../html +replace github.com/gofiber/template/django/v3 => ../django` + require.NoError(t, os.WriteFile(filepath.Join(dir, "go.mod"), []byte(modContent), 0o600)) + + var buf bytes.Buffer + cmd := newCmd(&buf) + require.NoError(t, v3.MigrateTemplateVersions(cmd, dir, nil, nil)) + + content := readFile(t, file) + assert.Contains(t, content, "github.com/gofiber/template/html/v3") + assert.Contains(t, content, "github.com/gofiber/template/django/v4") + assert.Contains(t, content, "github.com/gofiber/template/amber/v3") + assert.NotContains(t, content, "github.com/gofiber/template/html/v2") + assert.NotContains(t, content, "github.com/gofiber/template/django/v3") + + mod := readFile(t, filepath.Join(dir, "go.mod")) + assert.Contains(t, mod, "github.com/gofiber/template/html/v3 v3.0.0") + assert.Contains(t, mod, "github.com/gofiber/template/django/v4 v4.0.0") + assert.Contains(t, mod, "github.com/gofiber/template/amber/v3 v3.0.0") + assert.Contains(t, mod, "replace github.com/gofiber/template/html/v3 => ../html") + assert.Contains(t, mod, "replace github.com/gofiber/template/django/v4 => ../django") + + assert.Contains(t, buf.String(), "Migrated template package versions") +} + +func Test_MigrateTemplateVersions_ReplaceWithVersion(t *testing.T) { + dir := t.TempDir() + + _ = writeTempFile(t, dir, `package main +import _ "github.com/gofiber/template/html/v2" +`) + + modContent := `module example + +go 1.22 + +require github.com/gofiber/template/html/v2 v2.0.0 + +replace github.com/gofiber/template/html/v2 v2.0.0 => ../html` + require.NoError(t, os.WriteFile(filepath.Join(dir, "go.mod"), []byte(modContent), 0o600)) + + var buf bytes.Buffer + cmd := newCmd(&buf) + require.NoError(t, v3.MigrateTemplateVersions(cmd, dir, nil, nil)) + + mod := readFile(t, filepath.Join(dir, "go.mod")) + assert.Contains(t, mod, "replace github.com/gofiber/template/html/v3 v3.0.0 => ../html") + assert.NotContains(t, mod, "replace github.com/gofiber/template/html/v3 v2.0.0 => ../html") +} + +func Test_MigrateTemplateVersions_Idempotent(t *testing.T) { + dir := t.TempDir() + + file := writeTempFile(t, dir, `package main +import "github.com/gofiber/template/html/v3" +`) + + modContent := `module example + +go 1.22 + +require github.com/gofiber/template/html/v3 v3.0.0` + require.NoError(t, os.WriteFile(filepath.Join(dir, "go.mod"), []byte(modContent), 0o600)) + + var buf bytes.Buffer + cmd := newCmd(&buf) + require.NoError(t, v3.MigrateTemplateVersions(cmd, dir, nil, nil)) + firstContent := readFile(t, file) + firstMod := readFile(t, filepath.Join(dir, "go.mod")) + + require.NoError(t, v3.MigrateTemplateVersions(cmd, dir, nil, nil)) + secondContent := readFile(t, file) + secondMod := readFile(t, filepath.Join(dir, "go.mod")) + + assert.Equal(t, firstContent, secondContent) + assert.Equal(t, firstMod, secondMod) + assert.Empty(t, buf.String()) +}