Skip to content

Commit 9fd9c6f

Browse files
Antonis Kalipetisclaude
authored andcommitted
refactor(platformifier): use fs.FS and return file map
Abstract the filesystem using Go's standard fs.FS interface and change Platformify() to return a map[string][]byte of generated files instead of writing them to disk. This enables callers (like source-integration- apps) to operate on in-memory filesystems and handle file output themselves. Changes: - internal/utils: all file functions take fs.FS as first parameter, use fs.WalkDir/fs.ReadFile instead of os equivalents - internal/question/models: WorkingDirectory becomes fs.FS, add Cwd string field for the OS path - internal/question/*: update all callers for new utils signatures - platformifier: interface returns (map[string][]byte, error), generic/django/laravel collect files in map instead of writing - platformifier: remove custom FS interface (fs.go), mock (fs_mock_test), mock generator (platformifier_mock_test), and nextjs platformifier - platformifier: adapt all tests to use fstest.MapFS and new interface - commands/platformify.go: write returned file map to disk - validator: use os.DirFS for FindAllFiles call Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 409547d commit 9fd9c6f

28 files changed

Lines changed: 379 additions & 975 deletions

commands/platformify.go

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import (
55
"errors"
66
"fmt"
77
"io"
8+
"os"
9+
"path"
810

911
"github.com/spf13/cobra"
1012

@@ -39,11 +41,7 @@ func Platformify(ctx context.Context, stdout, stderr io.Writer, assets *vendoriz
3941
answers := models.NewAnswers()
4042
answers.Flavor, _ = ctx.Value(FlavorKey).(string)
4143
ctx = models.ToContext(ctx, answers)
42-
ctx = colors.ToContext(
43-
ctx,
44-
stdout,
45-
stderr,
46-
)
44+
ctx = colors.ToContext(ctx, stdout, stderr)
4745
q := questionnaire.New(
4846
&question.WorkingDirectory{},
4947
&question.FilesOverwrite{},
@@ -76,12 +74,24 @@ func Platformify(ctx context.Context, stdout, stderr io.Writer, assets *vendoriz
7674
input := answers.ToUserInput()
7775

7876
pfier := platformifier.New(input, assets.ConfigFlavor)
79-
err = pfier.Platformify(ctx)
77+
configFiles, err := pfier.Platformify(ctx)
8078
if err != nil {
8179
fmt.Fprintln(stderr, colors.Colorize(colors.ErrorCode, err.Error()))
8280
return fmt.Errorf("could not configure project: %w", err)
8381
}
8482

83+
for file, contents := range configFiles {
84+
filePath := path.Join(answers.Cwd, file)
85+
if err := os.MkdirAll(path.Dir(filePath), os.ModeDir|os.ModePerm); err != nil {
86+
fmt.Fprintf(stderr, "Could not create parent directories of file %s: %s\n", file, err)
87+
continue
88+
}
89+
if err := os.WriteFile(filePath, contents, 0o664); err != nil {
90+
fmt.Fprintf(stderr, "Could not write file %s: %s\n", file, err)
91+
continue
92+
}
93+
}
94+
8595
done := question.Done{}
8696
return done.Ask(ctx)
8797
}

internal/question/application_root.go

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package question
33
import (
44
"context"
55
"path"
6-
"path/filepath"
76

87
"github.com/platformsh/platformify/internal/question/models"
98
"github.com/platformsh/platformify/internal/utils"
@@ -20,28 +19,28 @@ func (q *ApplicationRoot) Ask(ctx context.Context) error {
2019
for _, dm := range answers.DependencyManagers {
2120
switch dm {
2221
case models.Composer:
23-
if composerPath := utils.FindFile(answers.WorkingDirectory, "composer.json"); composerPath != "" {
24-
answers.ApplicationRoot, _ = filepath.Rel(answers.WorkingDirectory, path.Dir(composerPath))
22+
if composerPath := utils.FindFile(answers.WorkingDirectory, "", "composer.json"); composerPath != "" {
23+
answers.ApplicationRoot = path.Dir(composerPath)
2524
return nil
2625
}
2726
case models.Npm, models.Yarn:
28-
if packagePath := utils.FindFile(answers.WorkingDirectory, "package.json"); packagePath != "" {
29-
answers.ApplicationRoot, _ = filepath.Rel(answers.WorkingDirectory, path.Dir(packagePath))
27+
if packagePath := utils.FindFile(answers.WorkingDirectory, "", "package.json"); packagePath != "" {
28+
answers.ApplicationRoot = path.Dir(packagePath)
3029
return nil
3130
}
3231
case models.Poetry:
33-
if pyProjectPath := utils.FindFile(answers.WorkingDirectory, "pyproject.toml"); pyProjectPath != "" {
34-
answers.ApplicationRoot, _ = filepath.Rel(answers.WorkingDirectory, path.Dir(pyProjectPath))
32+
if pyProjectPath := utils.FindFile(answers.WorkingDirectory, "", "pyproject.toml"); pyProjectPath != "" {
33+
answers.ApplicationRoot = path.Dir(pyProjectPath)
3534
return nil
3635
}
3736
case models.Pipenv:
38-
if pipfilePath := utils.FindFile(answers.WorkingDirectory, "Pipfile"); pipfilePath != "" {
39-
answers.ApplicationRoot, _ = filepath.Rel(answers.WorkingDirectory, path.Dir(pipfilePath))
37+
if pipfilePath := utils.FindFile(answers.WorkingDirectory, "", "Pipfile"); pipfilePath != "" {
38+
answers.ApplicationRoot = path.Dir(pipfilePath)
4039
return nil
4140
}
4241
case models.Pip:
43-
if requirementsPath := utils.FindFile(answers.WorkingDirectory, "requirements.txt"); requirementsPath != "" {
44-
answers.ApplicationRoot, _ = filepath.Rel(answers.WorkingDirectory, path.Dir(requirementsPath))
42+
if requirementsPath := utils.FindFile(answers.WorkingDirectory, "", "requirements.txt"); requirementsPath != "" {
43+
answers.ApplicationRoot = path.Dir(requirementsPath)
4544
return nil
4645
}
4746
}

internal/question/build_steps.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package question
33
import (
44
"context"
55
"fmt"
6-
"path"
76
"path/filepath"
87
"slices"
98

@@ -74,8 +73,9 @@ func (q *BuildSteps) Ask(ctx context.Context) error {
7473
)
7574
}
7675
if _, ok := utils.GetJSONValue(
76+
answers.WorkingDirectory,
7777
[]string{"scripts", "build"},
78-
path.Join(answers.WorkingDirectory, "package.json"),
78+
"package.json",
7979
true,
8080
); ok {
8181
if dm == models.Yarn {
@@ -100,7 +100,8 @@ func (q *BuildSteps) Ask(ctx context.Context) error {
100100
switch answers.Stack {
101101
case models.Django:
102102
if managePyPath := utils.FindFile(
103-
path.Join(answers.WorkingDirectory, answers.ApplicationRoot),
103+
answers.WorkingDirectory,
104+
answers.ApplicationRoot,
104105
managePyFile,
105106
); managePyPath != "" {
106107
prefix := ""
@@ -110,7 +111,7 @@ func (q *BuildSteps) Ask(ctx context.Context) error {
110111
prefix = "poetry run "
111112
}
112113

113-
managePyPath, _ = filepath.Rel(path.Join(answers.WorkingDirectory, answers.ApplicationRoot), managePyPath)
114+
managePyPath, _ = filepath.Rel(answers.ApplicationRoot, managePyPath)
114115
assets, _ := vendorization.FromContext(ctx)
115116
answers.BuildSteps = append(
116117
answers.BuildSteps,

internal/question/build_steps_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"reflect"
66
"testing"
7+
"testing/fstest"
78

89
"github.com/platformsh/platformify/internal/question/models"
910
)
@@ -37,6 +38,7 @@ func TestBuildSteps_Ask(t *testing.T) {
3738
Dependencies: map[string]map[string]string{},
3839
DependencyManagers: []models.DepManager{models.Yarn},
3940
Environment: map[string]string{},
41+
WorkingDirectory: fstest.MapFS{},
4042
}},
4143
buildSteps: []string{"yarn", "yarn exec next build"},
4244
wantErr: false,
@@ -50,6 +52,7 @@ func TestBuildSteps_Ask(t *testing.T) {
5052
Dependencies: map[string]map[string]string{},
5153
DependencyManagers: []models.DepManager{models.Npm},
5254
Environment: map[string]string{},
55+
WorkingDirectory: fstest.MapFS{},
5356
}},
5457
buildSteps: []string{"npm i", "npm exec next build"},
5558
wantErr: false,
@@ -63,6 +66,7 @@ func TestBuildSteps_Ask(t *testing.T) {
6366
Dependencies: map[string]map[string]string{},
6467
DependencyManagers: []models.DepManager{models.Bundler},
6568
Environment: map[string]string{},
69+
WorkingDirectory: fstest.MapFS{},
6670
}},
6771
buildSteps: []string{"bundle install"},
6872
wantErr: false,
@@ -76,6 +80,7 @@ func TestBuildSteps_Ask(t *testing.T) {
7680
Dependencies: map[string]map[string]string{},
7781
DependencyManagers: []models.DepManager{models.Bundler},
7882
Environment: map[string]string{},
83+
WorkingDirectory: fstest.MapFS{},
7984
}},
8085
buildSteps: []string{"bundle install", "bundle exec rails assets:precompile"},
8186
wantErr: false,

internal/question/dependency_manager.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -58,27 +58,27 @@ func (q *DependencyManager) Ask(ctx context.Context) error {
5858
}
5959
}()
6060

61-
if exists := utils.FileExists(answers.WorkingDirectory, poetryLockFile); exists {
61+
if exists := utils.FileExists(answers.WorkingDirectory, "", poetryLockFile); exists {
6262
answers.DependencyManagers = append(answers.DependencyManagers, models.Poetry)
63-
} else if exists := utils.FileExists(answers.WorkingDirectory, pipenvLockFile); exists {
63+
} else if exists := utils.FileExists(answers.WorkingDirectory, "", pipenvLockFile); exists {
6464
answers.DependencyManagers = append(answers.DependencyManagers, models.Pipenv)
65-
} else if exists := utils.FileExists(answers.WorkingDirectory, pipLockFile); exists {
65+
} else if exists := utils.FileExists(answers.WorkingDirectory, "", pipLockFile); exists {
6666
answers.DependencyManagers = append(answers.DependencyManagers, models.Pip)
6767
}
6868

69-
if exists := utils.FileExists(answers.WorkingDirectory, composerLockFile); exists {
69+
if exists := utils.FileExists(answers.WorkingDirectory, "", composerLockFile); exists {
7070
answers.DependencyManagers = append(answers.DependencyManagers, models.Composer)
7171
answers.Dependencies["php"] = map[string]string{"composer/composer": "^2"}
7272
}
7373

74-
if exists := utils.FileExists(answers.WorkingDirectory, yarnLockFileName); exists {
74+
if exists := utils.FileExists(answers.WorkingDirectory, "", yarnLockFileName); exists {
7575
answers.DependencyManagers = append(answers.DependencyManagers, models.Yarn)
7676
answers.Dependencies["nodejs"] = map[string]string{"yarn": "^1.22.0"}
77-
} else if exists := utils.FileExists(answers.WorkingDirectory, npmLockFileName); exists {
77+
} else if exists := utils.FileExists(answers.WorkingDirectory, "", npmLockFileName); exists {
7878
answers.DependencyManagers = append(answers.DependencyManagers, models.Npm)
7979
}
8080

81-
if exists := utils.FileExists(answers.WorkingDirectory, bundlerLockFile); exists {
81+
if exists := utils.FileExists(answers.WorkingDirectory, "", bundlerLockFile); exists {
8282
answers.DependencyManagers = append(answers.DependencyManagers, models.Bundler)
8383
}
8484

internal/question/deploy_command.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package question
33
import (
44
"context"
55
"fmt"
6-
"path"
76
"path/filepath"
87
"slices"
98

@@ -21,9 +20,9 @@ func (q *DeployCommand) Ask(ctx context.Context) error {
2120

2221
switch answers.Stack {
2322
case models.Django:
24-
managePyPath := utils.FindFile(path.Join(answers.WorkingDirectory, answers.ApplicationRoot), managePyFile)
23+
managePyPath := utils.FindFile(answers.WorkingDirectory, answers.ApplicationRoot, managePyFile)
2524
if managePyPath != "" {
26-
managePyPath, _ = filepath.Rel(path.Join(answers.WorkingDirectory, answers.ApplicationRoot), managePyPath)
25+
managePyPath, _ = filepath.Rel(answers.ApplicationRoot, managePyPath)
2726
prefix := ""
2827
if slices.Contains(answers.DependencyManagers, models.Pipenv) {
2928
prefix = "pipenv run "

internal/question/done.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ func (q *Done) Ask(ctx context.Context) error {
7878
),
7979
),
8080
)
81-
fmt.Fprintf(out, " $ git init %s\n", answers.WorkingDirectory)
81+
fmt.Fprintf(out, " $ git init %s\n", answers.Cwd)
8282
fmt.Fprintln(out, " $ git add .")
8383
fmt.Fprintf(out, " $ git commit -m 'Add %s configuration files'\n", assets.ServiceName)
8484
fmt.Fprintf(out, " $ %s project:set-remote\n", assets.Binary)

internal/question/files_overwrite.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@ package question
33
import (
44
"context"
55
"fmt"
6-
"os"
7-
"path/filepath"
6+
"io/fs"
87

98
"github.com/AlecAivazis/survey/v2"
109

@@ -30,7 +29,7 @@ func (q *FilesOverwrite) Ask(ctx context.Context) error {
3029
assets, _ := vendorization.FromContext(ctx)
3130
existingFiles := make([]string, 0, len(assets.ProprietaryFiles()))
3231
for _, p := range assets.ProprietaryFiles() {
33-
if st, err := os.Stat(filepath.Join(answers.WorkingDirectory, p)); err == nil && !st.IsDir() {
32+
if st, err := fs.Stat(answers.WorkingDirectory, p); err == nil && !st.IsDir() {
3433
existingFiles = append(existingFiles, p)
3534
}
3635
}
@@ -40,7 +39,7 @@ func (q *FilesOverwrite) Ask(ctx context.Context) error {
4039
stderr,
4140
colors.Colorize(
4241
colors.WarningCode,
43-
fmt.Sprintf("You are reconfiguring the project at %s.", answers.WorkingDirectory),
42+
fmt.Sprintf("You are reconfiguring the project at %s.", answers.Cwd),
4443
),
4544
)
4645
fmt.Fprintln(

internal/question/locations.go

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ package question
22

33
import (
44
"context"
5-
"path/filepath"
5+
"path"
66

77
"github.com/platformsh/platformify/internal/question/models"
88
"github.com/platformsh/platformify/internal/utils"
@@ -29,10 +29,9 @@ func (q *Locations) Ask(ctx context.Context) error {
2929
"passthru": "/index.php",
3030
"root": "",
3131
}
32-
if indexPath := utils.FindFile(answers.WorkingDirectory, "index.php"); indexPath != "" {
33-
indexRelPath, _ := filepath.Rel(answers.WorkingDirectory, indexPath)
34-
if filepath.Dir(indexRelPath) != "." {
35-
locations["root"] = filepath.Dir(indexRelPath)
32+
if indexPath := utils.FindFile(answers.WorkingDirectory, "", "index.php"); indexPath != "" {
33+
if path.Dir(indexPath) != "." {
34+
locations["root"] = path.Dir(indexPath)
3635
}
3736
}
3837
answers.Locations["/"] = locations

internal/question/models/answer.go

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package models
22

33
import (
44
"encoding/json"
5+
"io/fs"
56
"os"
67
"path/filepath"
78
"strings"
@@ -10,23 +11,24 @@ import (
1011
)
1112

1213
type Answers struct {
13-
Stack Stack `json:"stack"`
14-
Flavor string `json:"flavor"`
15-
Type RuntimeType `json:"type"`
16-
Name string `json:"name"`
17-
ApplicationRoot string `json:"application_root"`
18-
Environment map[string]string `json:"environment"`
19-
BuildSteps []string `json:"build_steps"`
20-
WebCommand string `json:"web_command"`
21-
SocketFamily SocketFamily `json:"socket_family"`
22-
DeployCommand []string `json:"deploy_command"`
23-
DependencyManagers []DepManager `json:"dependency_managers"`
24-
Dependencies map[string]map[string]string `json:"dependencies"`
25-
BuildFlavor string `json:"build_flavor"`
26-
Disk string `json:"disk"`
27-
Mounts map[string]map[string]string `json:"mounts"`
28-
Services []Service `json:"services"`
29-
WorkingDirectory string `json:"working_directory"`
14+
Stack Stack `json:"stack"`
15+
Flavor string `json:"flavor"`
16+
Type RuntimeType `json:"type"`
17+
Name string `json:"name"`
18+
ApplicationRoot string `json:"application_root"`
19+
Environment map[string]string `json:"environment"`
20+
BuildSteps []string `json:"build_steps"`
21+
WebCommand string `json:"web_command"`
22+
SocketFamily SocketFamily `json:"socket_family"`
23+
DeployCommand []string `json:"deploy_command"`
24+
DependencyManagers []DepManager `json:"dependency_managers"`
25+
Dependencies map[string]map[string]string `json:"dependencies"`
26+
BuildFlavor string `json:"build_flavor"`
27+
Disk string `json:"disk"`
28+
Mounts map[string]map[string]string `json:"mounts"`
29+
Services []Service `json:"services"`
30+
WorkingDirectory fs.FS
31+
Cwd string
3032
HasGit bool `json:"has_git"`
3133
FilesCreated []string `json:"files_created"`
3234
Locations map[string]map[string]interface{} `json:"locations"`
@@ -115,7 +117,6 @@ func (a *Answers) ToUserInput() *platformifier.UserInput {
115117

116118
return &platformifier.UserInput{
117119
Stack: getStack(a.Stack),
118-
Root: "",
119120
ApplicationRoot: filepath.Join(string(os.PathSeparator), a.ApplicationRoot),
120121
Name: a.Name,
121122
Type: a.Type.String(),

0 commit comments

Comments
 (0)