Skip to content

Commit 9131050

Browse files
committed
feat: normalize project directory names to kebab-case
The create command now converts app names to kebab-case (lowercase, dash-delimited, no special characters) when creating project directories. For example, "My App" becomes "my-app" instead of "My-App".
1 parent c449532 commit 9131050

2 files changed

Lines changed: 86 additions & 30 deletions

File tree

internal/pkg/create/create.go

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"os"
2323
"os/exec"
2424
"path/filepath"
25+
"regexp"
2526
"strings"
2627

2728
"github.com/go-git/go-git/v5"
@@ -166,15 +167,28 @@ func Create(ctx context.Context, clients *shared.ClientFactory, log *logger.Logg
166167
return appDirPath, nil
167168
}
168169

169-
// getAppDirName will validate and return the app's directory name
170+
// nonAlphanumericRe matches any character that is not a lowercase letter, digit, or dash.
171+
var nonAlphanumericRe = regexp.MustCompile(`[^a-z0-9-]+`)
172+
173+
// multiDashRe matches consecutive dashes.
174+
var multiDashRe = regexp.MustCompile(`-{2,}`)
175+
176+
// getAppDirName will validate and return the app's directory name in kebab-case
170177
func getAppDirName(appName string) (string, error) {
171178
if len(appName) <= 0 {
172179
return "", fmt.Errorf("app name is required")
173180
}
174181

175-
// trim whitespace
182+
// Normalize to kebab-case: lowercase, replace non-alphanumeric with dashes, collapse, and trim
176183
appName = strings.TrimSpace(appName)
177-
appName = strings.ReplaceAll(appName, " ", "-")
184+
appName = strings.ToLower(appName)
185+
appName = nonAlphanumericRe.ReplaceAllString(appName, "-")
186+
appName = multiDashRe.ReplaceAllString(appName, "-")
187+
appName = strings.Trim(appName, "-")
188+
189+
if len(appName) == 0 {
190+
return "", fmt.Errorf("app name must contain at least one alphanumeric character")
191+
}
178192

179193
// name cannot be a reserved word
180194
if goutils.Contains(reserved, appName, false) {

internal/pkg/create/create_test.go

Lines changed: 69 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -38,33 +38,75 @@ func TestCreate(t *testing.T) {
3838
}
3939

4040
func TestGetProjectDirectoryName(t *testing.T) {
41-
var appName string
42-
var err error
43-
44-
// Test with empty name returns an error
45-
appName, err = getAppDirName("")
46-
assert.Error(t, err, "should return an error for empty name")
47-
assert.Equal(t, "", appName)
48-
49-
// Test with app name
50-
appName, err = getAppDirName("my-app")
51-
assert.NoError(t, err, "should not return an error")
52-
assert.Equal(t, "my-app", appName, "should return 'my-app'")
53-
54-
// Test with a dot in the app name
55-
appName, err = getAppDirName(".my-app")
56-
assert.NoError(t, err, "should not return an error")
57-
assert.Equal(t, ".my-app", appName, "should return '.my-app'")
58-
59-
// Spaces replaced with hyphens
60-
appName, err = getAppDirName("my cool app")
61-
assert.NoError(t, err)
62-
assert.Equal(t, "my-cool-app", appName)
63-
64-
// Leading/trailing spaces trimmed
65-
appName, err = getAppDirName(" my-app ")
66-
assert.NoError(t, err)
67-
assert.Equal(t, "my-app", appName)
41+
tests := map[string]struct {
42+
input string
43+
expected string
44+
hasError bool
45+
}{
46+
"empty name returns error": {
47+
input: "",
48+
hasError: true,
49+
},
50+
"simple kebab-case name": {
51+
input: "my-app",
52+
expected: "my-app",
53+
},
54+
"spaces replaced with hyphens": {
55+
input: "my cool app",
56+
expected: "my-cool-app",
57+
},
58+
"leading and trailing spaces trimmed": {
59+
input: " my-app ",
60+
expected: "my-app",
61+
},
62+
"uppercase converted to lowercase": {
63+
input: "My Slack App",
64+
expected: "my-slack-app",
65+
},
66+
"mixed case normalized": {
67+
input: "My-Slack-App",
68+
expected: "my-slack-app",
69+
},
70+
"special characters replaced with dashes": {
71+
input: "my_app!@#test",
72+
expected: "my-app-test",
73+
},
74+
"consecutive special characters collapsed to single dash": {
75+
input: "my---app",
76+
expected: "my-app",
77+
},
78+
"leading and trailing special characters trimmed": {
79+
input: "---my-app---",
80+
expected: "my-app",
81+
},
82+
"dots converted to dashes": {
83+
input: ".my-app",
84+
expected: "my-app",
85+
},
86+
"only special characters returns error": {
87+
input: "!!!",
88+
hasError: true,
89+
},
90+
"numbers preserved": {
91+
input: "app123",
92+
expected: "app123",
93+
},
94+
"complex mixed input": {
95+
input: " My Cool App! (v2) ",
96+
expected: "my-cool-app-v2",
97+
},
98+
}
99+
for name, tc := range tests {
100+
t.Run(name, func(t *testing.T) {
101+
result, err := getAppDirName(tc.input)
102+
if tc.hasError {
103+
assert.Error(t, err)
104+
} else {
105+
assert.NoError(t, err)
106+
assert.Equal(t, tc.expected, result)
107+
}
108+
})
109+
}
68110
}
69111

70112
func TestGetAvailableDirectory(t *testing.T) {

0 commit comments

Comments
 (0)