Skip to content

Commit 0250e4f

Browse files
committed
Clarify --features warning to suggest apps init without --template
Co-authored-by: Isaac
1 parent 57c0e28 commit 0250e4f

1 file changed

Lines changed: 89 additions & 28 deletions

File tree

cmd/apps/init.go

Lines changed: 89 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import (
3131
"github.com/databricks/cli/libs/log"
3232
ignore "github.com/sabhiram/go-gitignore"
3333
"github.com/spf13/cobra"
34+
"go.yaml.in/yaml/v3"
3435
)
3536

3637
const (
@@ -40,6 +41,13 @@ const (
4041
appkitDefaultBranch = "main"
4142
appkitTemplateTagPfx = "template-v"
4243
defaultProfile = "DEFAULT"
44+
45+
// projectNamePlaceholder is the Go template variable used in template
46+
// directory names to stand in for the user-provided project name.
47+
projectNamePlaceholder = "{{.project_name}}"
48+
49+
// bundleConfigFile is the standard bundle configuration filename.
50+
bundleConfigFile = "databricks.yml"
4351
)
4452

4553
// normalizeVersion converts a version string to the template tag format "template-vX.X.X".
@@ -649,11 +657,11 @@ func isPreRenderedTemplate(templateDir string) bool {
649657
return false
650658
}
651659
for _, e := range entries {
652-
if e.IsDir() && strings.Contains(e.Name(), "{{.project_name}}") {
660+
if e.IsDir() && strings.Contains(e.Name(), projectNamePlaceholder) {
653661
return false
654662
}
655663
}
656-
data, err := os.ReadFile(filepath.Join(templateDir, "databricks.yml"))
664+
data, err := os.ReadFile(filepath.Join(templateDir, bundleConfigFile))
657665
if err != nil {
658666
// No databricks.yml — can't tell, assume normal template.
659667
return false
@@ -662,8 +670,8 @@ func isPreRenderedTemplate(templateDir string) bool {
662670
}
663671

664672
// replaceProjectName updates the project name in key files after copying a
665-
// pre-rendered template. It reads the original bundle name from
666-
// databricks.yml and replaces it with newName in databricks.yml and
673+
// pre-rendered template. It sets bundle.name and the first
674+
// resources.apps.*.name in databricks.yml, and the name field in
667675
// package.json.
668676
func replaceProjectName(destDir, newName string) error {
669677
// Update package.json name field via JSON round-trip.
@@ -681,34 +689,87 @@ func replaceProjectName(destDir, newName string) error {
681689
}
682690
}
683691

684-
// Update databricks.yml: bundle name and app name.
685-
ymlPath := filepath.Join(destDir, "databricks.yml")
692+
// Update databricks.yml using yaml.Node to preserve comments and formatting.
693+
ymlPath := filepath.Join(destDir, bundleConfigFile)
686694
data, err := os.ReadFile(ymlPath)
687695
if err != nil {
688-
return nil
696+
return err
689697
}
690-
content := string(data)
691698

692-
// Read the original bundle name so we can do a targeted replace of the
693-
// app resource name that usually matches it.
694-
origName := ""
695-
for _, line := range strings.Split(content, "\n") {
696-
trimmed := strings.TrimSpace(line)
697-
if strings.HasPrefix(trimmed, "name:") && origName == "" {
698-
origName = strings.TrimSpace(strings.TrimPrefix(trimmed, "name:"))
699-
origName = strings.Trim(origName, "\"'")
700-
break
699+
var doc yaml.Node
700+
if err := yaml.Unmarshal(data, &doc); err != nil {
701+
return fmt.Errorf("parse %s: %w", bundleConfigFile, err)
702+
}
703+
704+
setYAMLValue(&doc, []string{"bundle", "name"}, newName)
705+
setFirstAppName(&doc, newName)
706+
707+
var buf bytes.Buffer
708+
enc := yaml.NewEncoder(&buf)
709+
enc.SetIndent(2)
710+
if err := enc.Encode(&doc); err != nil {
711+
return fmt.Errorf("encode %s: %w", bundleConfigFile, err)
712+
}
713+
enc.Close()
714+
715+
return os.WriteFile(ymlPath, buf.Bytes(), 0o644)
716+
}
717+
718+
// setYAMLValue walks a yaml.Node tree following the given key path and
719+
// replaces the leaf scalar value. It handles both document and mapping nodes.
720+
func setYAMLValue(node *yaml.Node, keys []string, value string) {
721+
if node.Kind == yaml.DocumentNode && len(node.Content) > 0 {
722+
setYAMLValue(node.Content[0], keys, value)
723+
return
724+
}
725+
if node.Kind != yaml.MappingNode || len(keys) == 0 {
726+
return
727+
}
728+
for i := 0; i < len(node.Content)-1; i += 2 {
729+
if node.Content[i].Value == keys[0] {
730+
if len(keys) == 1 {
731+
node.Content[i+1].Value = value
732+
// Clear any explicit style so the encoder picks the
733+
// simplest representation for the new value.
734+
node.Content[i+1].Style = 0
735+
return
736+
}
737+
setYAMLValue(node.Content[i+1], keys[1:], value)
738+
return
701739
}
702740
}
741+
}
703742

704-
// Replace the bundle name line (first occurrence of "name:" under "bundle:").
705-
if origName != "" {
706-
content = strings.Replace(content, "name: "+origName, "name: "+newName, 1)
707-
// Replace the quoted app name in the resources section.
708-
content = strings.Replace(content, "name: \""+origName+"\"", "name: \""+newName+"\"", 1)
743+
// setFirstAppName sets the name field of the first app entry under
744+
// resources.apps in a yaml.Node tree.
745+
func setFirstAppName(node *yaml.Node, name string) {
746+
if node.Kind == yaml.DocumentNode && len(node.Content) > 0 {
747+
setFirstAppName(node.Content[0], name)
748+
return
749+
}
750+
if node.Kind != yaml.MappingNode {
751+
return
709752
}
753+
// Find resources → apps → first entry → name.
754+
appsNode := yamlMapLookup(yamlMapLookup(node, "resources"), "apps")
755+
if appsNode == nil || appsNode.Kind != yaml.MappingNode || len(appsNode.Content) < 2 {
756+
return
757+
}
758+
// First app entry is at Content[1] (Content[0] is the key).
759+
setYAMLValue(appsNode.Content[1], []string{"name"}, name)
760+
}
710761

711-
return os.WriteFile(ymlPath, []byte(content), 0o644)
762+
// yamlMapLookup returns the value node for a key in a mapping node, or nil.
763+
func yamlMapLookup(node *yaml.Node, key string) *yaml.Node {
764+
if node == nil || node.Kind != yaml.MappingNode {
765+
return nil
766+
}
767+
for i := 0; i < len(node.Content)-1; i += 2 {
768+
if node.Content[i].Value == key {
769+
return node.Content[i+1]
770+
}
771+
}
772+
return nil
712773
}
713774

714775
// findProjectSrcDir locates the actual source directory inside a template.
@@ -719,7 +780,7 @@ func findProjectSrcDir(templateDir string) string {
719780
return templateDir
720781
}
721782
for _, e := range entries {
722-
if e.IsDir() && strings.Contains(e.Name(), "{{.project_name}}") {
783+
if e.IsDir() && strings.Contains(e.Name(), projectNamePlaceholder) {
723784
return filepath.Join(templateDir, e.Name())
724785
}
725786
}
@@ -1170,9 +1231,9 @@ func runCreate(ctx context.Context, opts createOptions) error {
11701231
}
11711232
for _, p := range opts.plugins {
11721233
if !mandatory[p] {
1173-
log.Warnf(ctx, "Feature %q is not supported by this pre-rendered template and will be ignored."+
1174-
" Only .env will include its configuration."+
1175-
" To use all features dynamically, use the default AppKit template instead.", p)
1234+
log.Warnf(ctx, "Adding feature %q to a pre-rendered template is not currently supported.\n"+
1235+
"To add it manually, register the plugin in server/server.ts and run `npx @databricks/appkit plugin sync --write`.\n"+
1236+
"To use all features dynamically, run `databricks apps init` without --template.", p)
11761237
}
11771238
}
11781239
}
@@ -1532,7 +1593,7 @@ func copyTemplate(ctx context.Context, src, dest string, vars templateVars) (int
15321593
return 0, err
15331594
}
15341595
for _, e := range entries {
1535-
if e.IsDir() && strings.Contains(e.Name(), "{{.project_name}}") {
1596+
if e.IsDir() && strings.Contains(e.Name(), projectNamePlaceholder) {
15361597
srcProjectDir = filepath.Join(src, e.Name())
15371598
break
15381599
}

0 commit comments

Comments
 (0)