Skip to content

Commit 57c0e28

Browse files
committed
apps init: support pre-rendered templates
When `apps init --template` is pointed at an already-materialised project (e.g. the app-templates/appkit-* directories), the command no longer requires `--set` flags for resources that are already configured in the template's databricks.yml. Changes: - Detect pre-rendered templates by checking for absence of `{{.project_name}}` directories and Go template syntax in databricks.yml - Skip resource validation for pre-rendered templates - Replace the template name with the user-provided project name in databricks.yml and package.json after copying - Warn when `--features` adds plugins that the pre-rendered template cannot inject (they appear in .env but not databricks.yml/server.ts) - Skip interactive plugin/resource prompting for pre-rendered templates Co-authored-by: Isaac
1 parent 33a489c commit 57c0e28

1 file changed

Lines changed: 121 additions & 2 deletions

File tree

cmd/apps/init.go

Lines changed: 121 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -637,6 +637,80 @@ func commitInPlace() (string, error) {
637637
return appName, nil
638638
}
639639

640+
// isPreRenderedTemplate returns true when the template directory contains an
641+
// already-materialised project (e.g. one of the app-templates/appkit-* repos)
642+
// rather than a raw Go-template tree.
643+
//
644+
// Detection: there is no {{.project_name}} subdirectory AND the databricks.yml
645+
// file (if present) contains no Go template syntax ("{{").
646+
func isPreRenderedTemplate(templateDir string) bool {
647+
entries, err := os.ReadDir(templateDir)
648+
if err != nil {
649+
return false
650+
}
651+
for _, e := range entries {
652+
if e.IsDir() && strings.Contains(e.Name(), "{{.project_name}}") {
653+
return false
654+
}
655+
}
656+
data, err := os.ReadFile(filepath.Join(templateDir, "databricks.yml"))
657+
if err != nil {
658+
// No databricks.yml — can't tell, assume normal template.
659+
return false
660+
}
661+
return !strings.Contains(string(data), "{{")
662+
}
663+
664+
// 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
667+
// package.json.
668+
func replaceProjectName(destDir, newName string) error {
669+
// Update package.json name field via JSON round-trip.
670+
pkgPath := filepath.Join(destDir, "package.json")
671+
if data, err := os.ReadFile(pkgPath); err == nil {
672+
var pkg map[string]any
673+
if err := json.Unmarshal(data, &pkg); err == nil {
674+
pkg["name"] = newName
675+
out, err := json.MarshalIndent(pkg, "", " ")
676+
if err == nil {
677+
// Preserve trailing newline convention.
678+
out = append(out, '\n')
679+
_ = os.WriteFile(pkgPath, out, 0o644)
680+
}
681+
}
682+
}
683+
684+
// Update databricks.yml: bundle name and app name.
685+
ymlPath := filepath.Join(destDir, "databricks.yml")
686+
data, err := os.ReadFile(ymlPath)
687+
if err != nil {
688+
return nil
689+
}
690+
content := string(data)
691+
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
701+
}
702+
}
703+
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)
709+
}
710+
711+
return os.WriteFile(ymlPath, []byte(content), 0o644)
712+
}
713+
640714
// findProjectSrcDir locates the actual source directory inside a template.
641715
// Templates may nest their content inside a {{.project_name}} directory.
642716
func findProjectSrcDir(templateDir string) string {
@@ -981,6 +1055,13 @@ func runCreate(ctx context.Context, opts createOptions) error {
9811055
}
9821056
}
9831057

1058+
// Detect whether this is a pre-rendered template (already-materialised
1059+
// project with no Go template placeholders in databricks.yml).
1060+
preRendered := isPreRenderedTemplate(templateDir)
1061+
if preRendered {
1062+
log.Debugf(ctx, "Detected pre-rendered template at %s", templateDir)
1063+
}
1064+
9841065
// Start npm install in the background so it runs while the user answers prompts.
9851066
// This is a Node.js-only optimisation — non-Node templates skip this.
9861067
srcProjectDir := findProjectSrcDir(templateDir)
@@ -1010,7 +1091,17 @@ func runCreate(ctx context.Context, opts createOptions) error {
10101091
// Skip deploy/run prompts if in flags mode or if deploy/run flags were explicitly set
10111092
skipDeployRunPrompt := flagsMode || opts.deployChanged || opts.runChanged
10121093

1013-
if isInteractive && !opts.pluginsChanged && !flagsMode {
1094+
if preRendered {
1095+
// Pre-rendered templates already have their plugins configured.
1096+
// Skip plugin/resource prompting entirely — just use mandatory plugins.
1097+
if isInteractive && !skipDeployRunPrompt {
1098+
var err error
1099+
shouldDeploy, runMode, err = prompt.PromptForDeployAndRun(ctx)
1100+
if err != nil {
1101+
return err
1102+
}
1103+
}
1104+
} else if isInteractive && !opts.pluginsChanged && !flagsMode {
10141105
// Interactive mode without --plugins flag: prompt for plugins, dependencies, description
10151106
config, err := promptForPluginsAndDeps(ctx, m, selectedPlugins, skipDeployRunPrompt, opts.autoApprove)
10161107
if err != nil {
@@ -1068,8 +1159,28 @@ func runCreate(ctx context.Context, opts createOptions) error {
10681159
// Always include mandatory plugins regardless of user selection or flags.
10691160
selectedPlugins = appendUnique(selectedPlugins, m.GetMandatoryPluginNames()...)
10701161

1162+
// Warn when --features adds plugins that the pre-rendered template
1163+
// cannot inject (they'll appear in .env but not in databricks.yml,
1164+
// server.ts, or app.yaml).
1165+
if preRendered && opts.pluginsChanged {
1166+
mandatoryNames := m.GetMandatoryPluginNames()
1167+
mandatory := make(map[string]bool, len(mandatoryNames))
1168+
for _, n := range mandatoryNames {
1169+
mandatory[n] = true
1170+
}
1171+
for _, p := range opts.plugins {
1172+
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)
1176+
}
1177+
}
1178+
}
1179+
10711180
// In flags/non-interactive mode, resolve derived values and validate resources.
1072-
if flagsMode || !isInteractive {
1181+
// Skip validation for pre-rendered templates — their resources are already
1182+
// configured in databricks.yml.
1183+
if !preRendered && (flagsMode || !isInteractive) {
10731184
resources := m.CollectResources(selectedPlugins)
10741185

10751186
// Resolve derived values for resources that support it.
@@ -1214,6 +1325,14 @@ func runCreate(ctx context.Context, opts createOptions) error {
12141325
}
12151326
projectCreated = true // From here on, cleanup on failure
12161327

1328+
// For pre-rendered templates, update the project name in key files since
1329+
// Go template substitution had no placeholders to fill.
1330+
if preRendered {
1331+
if err := replaceProjectName(destDir, opts.name); err != nil {
1332+
log.Warnf(ctx, "Could not update project name in output files: %v", err)
1333+
}
1334+
}
1335+
12171336
// Get absolute path
12181337
absOutputDir, err := filepath.Abs(destDir)
12191338
if err != nil {

0 commit comments

Comments
 (0)