diff --git a/internal/app/app.go b/internal/app/app.go index 9ed01bf5..4b1e5d35 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -48,16 +48,26 @@ func NewClient( // UpdateDefaultProjectFiles should update any project specific files if any func UpdateDefaultProjectFiles(fs afero.Fs, dirPath string, appDirName string) error { - var filenames = []string{"manifest.json", "manifest.js", "manifest.ts"} + // Files and their corresponding app name replacement functions + projectFiles := []struct { + filename string + replacer func([]byte, string) []byte + }{ + {"manifest.json", regexReplaceAppNameInManifest}, + {"manifest.js", regexReplaceAppNameInManifest}, + {"manifest.ts", regexReplaceAppNameInManifest}, + {"package.json", regexReplaceAppNameInPackageJSON}, + {"pyproject.toml", regexReplaceAppNameInPyprojectToml}, + } - for _, filename := range filenames { - filePath := filepath.Join(dirPath, filename) + for _, pf := range projectFiles { + filePath := filepath.Join(dirPath, pf.filename) fileData, err := afero.ReadFile(fs, filePath) if err != nil { continue } - fileData = regexReplaceAppNameInManifest(fileData, appDirName) + fileData = pf.replacer(fileData, appDirName) if err := afero.WriteFile(fs, filePath, fileData, 0644); err != nil { return err } @@ -149,3 +159,25 @@ func regexReplaceAppNameInManifest(src []byte, appName string) []byte { return srcUpdated } + +// regexReplaceAppNameInPackageJSON replaces the top-level "name" field in a package.json file +func regexReplaceAppNameInPackageJSON(src []byte, appName string) []byte { + re := regexp.MustCompile(`(?m)^(\s*"name"\s*:\s*")([^"]*)(")`) + loc := re.FindSubmatchIndex(src) + if loc == nil { + return src + } + // loc[4]:loc[5] is capture group 2 — the name value to replace + result := make([]byte, 0, len(src)) + result = append(result, src[:loc[4]]...) + result = append(result, []byte(appName)...) + result = append(result, src[loc[5]:]...) + return result +} + +// regexReplaceAppNameInPyprojectToml replaces the "name" field under the [project] section in a pyproject.toml file +func regexReplaceAppNameInPyprojectToml(src []byte, appName string) []byte { + re := regexp.MustCompile(`(\[project\][^\[]*?name\s*=\s*")([^"]*)(")`) + repl := fmt.Sprintf("${1}%s${3}", appName) + return re.ReplaceAll(src, []byte(repl)) +} diff --git a/internal/app/app_test.go b/internal/app/app_test.go index eae92335..c4720846 100644 --- a/internal/app/app_test.go +++ b/internal/app/app_test.go @@ -74,6 +74,40 @@ func Test_App_UpdateDefaultProjectFiles(t *testing.T) { }, expectedErrorType: nil, }, + "package.json file exists": { + appDirName: "vibrant-butterfly-1234", + existingFiles: map[string]string{ + "package.json": string(testdata.PackageJSON), + }, + expectedFiles: map[string]string{ + "package.json": string(testdata.PackageJSONAppName), + }, + expectedErrorType: nil, + }, + "pyproject.toml file exists": { + appDirName: "vibrant-butterfly-1234", + existingFiles: map[string]string{ + "pyproject.toml": string(testdata.PyprojectTOML), + }, + expectedFiles: map[string]string{ + "pyproject.toml": string(testdata.PyprojectTOMLAppName), + }, + expectedErrorType: nil, + }, + "Multiple project files exist": { + appDirName: "vibrant-butterfly-1234", + existingFiles: map[string]string{ + "manifest.json": string(testdata.ManifestJSON), + "package.json": string(testdata.PackageJSON), + "pyproject.toml": string(testdata.PyprojectTOML), + }, + expectedFiles: map[string]string{ + "manifest.json": string(testdata.ManifestJSONAppName), + "package.json": string(testdata.PackageJSONAppName), + "pyproject.toml": string(testdata.PyprojectTOMLAppName), + }, + expectedErrorType: nil, + }, "No manifest files exist": { appDirName: "vibrant-butterfly-1234", existingFiles: map[string]string{}, @@ -161,3 +195,160 @@ func Test_RegexReplaceAppNameInManifest(t *testing.T) { }) } } + +func Test_RegexReplaceAppNameInPackageJSON(t *testing.T) { + tests := map[string]struct { + src []byte + appName string + expectedSrc []byte + }{ + "package.json name is replaced": { + src: testdata.PackageJSON, + appName: "vibrant-butterfly-1234", + expectedSrc: testdata.PackageJSONAppName, + }, + "only top-level name is replaced not nested config name": { + src: []byte(`{ + "name": "bolt-app-template", + "version": "1.0.0", + "description": "A Slack app built with Bolt", + "main": "app.js", + "scripts": { + "start": "node app.js" + }, + "dependencies": { + "@slack/bolt": "^4.0.0" + }, + "config": { + "name": "local-server-name", + "host": "localhost", + "port": "8080" + } +} +`), + appName: "vibrant-butterfly-1234", + expectedSrc: []byte(`{ + "name": "vibrant-butterfly-1234", + "version": "1.0.0", + "description": "A Slack app built with Bolt", + "main": "app.js", + "scripts": { + "start": "node app.js" + }, + "dependencies": { + "@slack/bolt": "^4.0.0" + }, + "config": { + "name": "local-server-name", + "host": "localhost", + "port": "8080" + } +} +`), + }, + "no name field leaves input unchanged": { + src: []byte(`{ + "version": "1.0.0" +} +`), + appName: "my-app", + expectedSrc: []byte(`{ + "version": "1.0.0" +} +`), + }, + "empty name value is replaced": { + src: []byte("{\n \"name\": \"\"\n}\n"), + appName: "my-app", + expectedSrc: []byte("{\n \"name\": \"my-app\"\n}\n"), + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + actualSrc := regexReplaceAppNameInPackageJSON(tc.src, tc.appName) + require.Equal(t, tc.expectedSrc, actualSrc) + }) + } +} + +func Test_RegexReplaceAppNameInPyprojectToml(t *testing.T) { + tests := map[string]struct { + src []byte + appName string + expectedSrc []byte + }{ + "pyproject.toml name is replaced": { + src: testdata.PyprojectTOML, + appName: "vibrant-butterfly-1234", + expectedSrc: testdata.PyprojectTOMLAppName, + }, + "only project section name is replaced not project.scripts name": { + src: []byte(`[project] +name = "bolt-python-ai-agent-template" +version = "0.1.0" +requires-python = ">=3.9" +dependencies = [ + "slack-sdk==3.40.0", + "slack-bolt==1.27.0", + "slack-cli-hooks<1.0.0", +] + +[tool.ruff] +[tool.ruff.lint] +[tool.ruff.format] + +[tool.pytest.ini_options] +testpaths = ["tests"] + +[project.scripts] +name = "my_package.name:main_function" +`), + appName: "vibrant-butterfly-1234", + expectedSrc: []byte(`[project] +name = "vibrant-butterfly-1234" +version = "0.1.0" +requires-python = ">=3.9" +dependencies = [ + "slack-sdk==3.40.0", + "slack-bolt==1.27.0", + "slack-cli-hooks<1.0.0", +] + +[tool.ruff] +[tool.ruff.lint] +[tool.ruff.format] + +[tool.pytest.ini_options] +testpaths = ["tests"] + +[project.scripts] +name = "my_package.name:main_function" +`), + }, + "no project section leaves input unchanged": { + src: []byte(`[tool.ruff] +name = "should-not-change" +`), + appName: "my-app", + expectedSrc: []byte(`[tool.ruff] +name = "should-not-change" +`), + }, + "empty name value is replaced": { + src: []byte(`[project]` + "\n" + `name = ""` + "\n"), + appName: "my-app", + expectedSrc: []byte(`[project]` + "\n" + `name = "my-app"` + "\n"), + }, + "extra whitespace around equals sign": { + src: []byte(`[project]` + "\n" + `name = "old-name"` + "\n"), + appName: "new-name", + expectedSrc: []byte(`[project]` + "\n" + `name = "new-name"` + "\n"), + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + actualSrc := regexReplaceAppNameInPyprojectToml(tc.src, tc.appName) + require.Equal(t, tc.expectedSrc, actualSrc) + }) + } +} diff --git a/test/testdata/package-app-name.json b/test/testdata/package-app-name.json new file mode 100644 index 00000000..765a3bec --- /dev/null +++ b/test/testdata/package-app-name.json @@ -0,0 +1,12 @@ +{ + "name": "vibrant-butterfly-1234", + "version": "1.0.0", + "description": "A Slack app built with Bolt", + "main": "app.js", + "scripts": { + "start": "node app.js" + }, + "dependencies": { + "@slack/bolt": "^4.0.0" + } +} diff --git a/test/testdata/package.json b/test/testdata/package.json new file mode 100644 index 00000000..816f3564 --- /dev/null +++ b/test/testdata/package.json @@ -0,0 +1,12 @@ +{ + "name": "bolt-app-template", + "version": "1.0.0", + "description": "A Slack app built with Bolt", + "main": "app.js", + "scripts": { + "start": "node app.js" + }, + "dependencies": { + "@slack/bolt": "^4.0.0" + } +} diff --git a/test/testdata/pyproject-app-name.toml b/test/testdata/pyproject-app-name.toml new file mode 100644 index 00000000..7c5c49f7 --- /dev/null +++ b/test/testdata/pyproject-app-name.toml @@ -0,0 +1,17 @@ +[project] +name = "vibrant-butterfly-1234" +version = "0.1.0" +requires-python = ">=3.9" +dependencies = [ + "slack-sdk==3.40.0", + "slack-bolt==1.27.0", + "slack-cli-hooks<1.0.0", +] + +# This comment appears before more details +[tool.ruff] +[tool.ruff.lint] +[tool.ruff.format] + +[tool.pytest.ini_options] +testpaths = ["tests"] diff --git a/test/testdata/pyproject.toml b/test/testdata/pyproject.toml new file mode 100644 index 00000000..5527a4c3 --- /dev/null +++ b/test/testdata/pyproject.toml @@ -0,0 +1,17 @@ +[project] +name = "bolt-python-ai-agent-template" +version = "0.1.0" +requires-python = ">=3.9" +dependencies = [ + "slack-sdk==3.40.0", + "slack-bolt==1.27.0", + "slack-cli-hooks<1.0.0", +] + +# This comment appears before more details +[tool.ruff] +[tool.ruff.lint] +[tool.ruff.format] + +[tool.pytest.ini_options] +testpaths = ["tests"] diff --git a/test/testdata/testdata.go b/test/testdata/testdata.go index 06985b67..5f818a3c 100644 --- a/test/testdata/testdata.go +++ b/test/testdata/testdata.go @@ -27,3 +27,15 @@ var ManifestSDKTS []byte //go:embed manifest-sdk-app-name.ts var ManifestSDKTSAppName []byte + +//go:embed package.json +var PackageJSON []byte + +//go:embed package-app-name.json +var PackageJSONAppName []byte + +//go:embed pyproject.toml +var PyprojectTOML []byte + +//go:embed pyproject-app-name.toml +var PyprojectTOMLAppName []byte