diff --git a/cli/cmd/encore/app/create.go b/cli/cmd/encore/app/create.go index 192aab4bc0..cf7674c30b 100644 --- a/cli/cmd/encore/app/create.go +++ b/cli/cmd/encore/app/create.go @@ -118,7 +118,6 @@ func promptRunApp() bool { // createApp is the implementation of the "encore app create" command. func createApp(ctx context.Context, name, template string) (err error) { var lang language - var tutorial bool defer func() { // We need to send the telemetry synchronously to ensure it's sent before the command exits. telemetry.SendSync("app.create", map[string]any{ @@ -133,7 +132,7 @@ func createApp(ctx context.Context, name, template string) (err error) { promptAccountCreation() if name == "" || template == "" { - name, template, lang, tutorial = selectTemplate(name, template, false) + name, template, lang = selectTemplate(name, template, false) } // Treat the special name "empty" as the empty app template // (the rest of the code assumes that's the empty string). @@ -194,26 +193,20 @@ func createApp(ctx context.Context, name, template string) (err error) { _, err = conf.CurrentUser() loggedIn := err == nil + exCfg, ok := parseExampleConfig(name) + if ok { + _ = os.Remove(exampleJSONPath(name)) + } var app *platform.App if loggedIn && createAppOnPlatform { s := spinner.New(spinner.CharSets[14], 100*time.Millisecond) s.Prefix = "Creating app on encore.dev " s.Start() - - exCfg, ok := parseExampleConfig(name) app, err = createAppOnServer(name, exCfg) s.Stop() if err != nil { return fmt.Errorf("creating app on encore.dev: %v", err) } - - // Remove the example.json file if the app was successfully created. - if ok { - _ = os.Remove(exampleJSONPath(name)) - } - } else { - // Remove the example config file since we're not creating the app on the platform. - _ = os.Remove(exampleJSONPath(name)) } encoreAppPath := filepath.Join(name, "encore.app") @@ -280,7 +273,7 @@ func createApp(ctx context.Context, name, template string) (err error) { daemon := cmdutil.ConnectDaemon(ctx) _, err = daemon.CreateApp(ctx, &daemonpb.CreateAppRequest{ AppRoot: appRoot, - Tutorial: tutorial, + Tutorial: exCfg.Tutorial, Template: template, }) if err != nil { @@ -584,6 +577,7 @@ func rewritePlaceholder(path string, info fs.DirEntry, app *platform.App) error // exampleConfig is the optional configuration file for example apps. type exampleConfig struct { InitialSecrets map[string]string `json:"initial_secrets"` + Tutorial bool `json:"tutorial"` } func parseExampleConfig(repoPath string) (cfg exampleConfig, exists bool) { diff --git a/cli/cmd/encore/app/create_form.go b/cli/cmd/encore/app/create_form.go index 02573967a8..28bd330293 100644 --- a/cli/cmd/encore/app/create_form.go +++ b/cli/cmd/encore/app/create_form.go @@ -8,6 +8,7 @@ import ( "net/http" "os" "strings" + "sync" "time" "github.com/charmbracelet/bubbles/list" @@ -40,7 +41,6 @@ type templateItem struct { Desc string `json:"desc"` Template string `json:"template"` Lang language `json:"lang"` - Tutorial bool `json:"tutorial"` } func (i templateItem) Title() string { return i.ItemTitle } @@ -412,10 +412,10 @@ func (m templateListModel) SelectedItem() (templateItem, bool) { return templateItem{}, false } -func selectTemplate(inputName, inputTemplate string, skipShowingTemplate bool) (appName, template string, selectedLang language, tutorial bool) { +func selectTemplate(inputName, inputTemplate string, skipShowingTemplate bool) (appName, template string, selectedLang language) { // If we have both name and template already, return them. if inputName != "" && inputTemplate != "" { - return inputName, inputTemplate, "", false + return inputName, inputTemplate, "" } var lang languageSelectModel @@ -528,10 +528,9 @@ func selectTemplate(inputName, inputTemplate string, skipShowingTemplate bool) ( cmdutil.Fatal("no template selected") } template = sel.Template - tutorial = sel.Tutorial } - return appName, template, res.lang.Selected(), tutorial + return appName, template, res.lang.Selected() } type langItem struct { @@ -567,8 +566,75 @@ func (lang language) Display() string { type loadedTemplates []templateItem -func loadTutorials(ctx context.Context) []templateItem { - url := "https://raw.githubusercontent.com/encoredev/examples/main/cli-tutorials.json" +var defaultTutorials = []templateItem{ + { + ItemTitle: "Intro to Encore.ts", + Desc: "An interactive tutorial", + Template: "ts/introduction", + Lang: "ts", + }, +} + +var defaultTemplates = []templateItem{ + { + ItemTitle: "Hello World", + Desc: "A simple REST API", + Template: "hello-world", + Lang: "go", + }, + { + ItemTitle: "Hello World", + Desc: "A simple REST API", + Template: "ts/hello-world", + Lang: "ts", + }, + { + ItemTitle: "Uptime Monitor", + Desc: "Microservices, SQL Databases, Pub/Sub, Cron Jobs", + Template: "uptime", + Lang: "go", + }, + { + ItemTitle: "Uptime Monitor", + Desc: "Microservices, SQL Databases, Pub/Sub, Cron Jobs", + Template: "ts/uptime", + Lang: "ts", + }, + { + ItemTitle: "GraphQL", + Desc: "GraphQL API, Microservices, SQL Database", + Template: "graphql", + Lang: "go", + }, + { + ItemTitle: "URL Shortener", + Desc: "REST API, SQL Database", + Template: "url-shortener", + Lang: "go", + }, + { + ItemTitle: "URL Shortener", + Desc: "REST API, SQL Database", + Template: "ts/url-shortener", + Lang: "ts", + }, + { + ItemTitle: "Empty app", + Desc: "Start from scratch (experienced users only)", + Template: "", + Lang: "go", + }, + { + ItemTitle: "Empty app", + Desc: "Start from scratch (experienced users only)", + Template: "ts/empty", + Lang: "ts", + }, +} + +func fetchTemplates(url string, defaults []templateItem) []templateItem { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() if req, err := http.NewRequestWithContext(ctx, "GET", url, nil); err == nil { if resp, err := http.DefaultClient.Do(req); err == nil { if data, err := io.ReadAll(resp.Body); err == nil { @@ -582,113 +648,24 @@ func loadTutorials(ctx context.Context) []templateItem { } } } - return []templateItem{ - { - ItemTitle: "Intro to Encore.ts", - Desc: "An interactive tutorial", - Template: "ts/introduction", - Lang: "ts", - Tutorial: true, - }, - } - + return defaults } func loadTemplates() tea.Msg { - // Load the templates. - templates := (func() []templateItem { - // Get the list of templates from GitHub - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - url := "https://raw.githubusercontent.com/encoredev/examples/main/cli-templates.json" - if req, err := http.NewRequestWithContext(ctx, "GET", url, nil); err == nil { - if resp, err := http.DefaultClient.Do(req); err == nil { - if data, err := io.ReadAll(resp.Body); err == nil { - if data, err = hujson.Standardize(data); err == nil { - var items []templateItem - if err := json.Unmarshal(data, &items); err == nil && len(items) > 0 { - for i, it := range items { - if it.Lang == "" { - items[i].Lang = languageGo - if strings.Contains(it.Template, "ts/") || strings.Contains(strings.ToLower(it.ItemTitle), "typescript") { - items[i].Lang = languageTS - } - } - } - return append(loadTutorials(ctx), items...) - } - } - } - } - } - - // Return a precompiled list of default items in case we can't read them from GitHub. - return []templateItem{ - { - ItemTitle: "Intro to Encore.ts", - Desc: "An interactive tutorial", - Template: "ts/introduction", - Lang: "ts", - Tutorial: true, - }, - { - ItemTitle: "Hello World", - Desc: "A simple REST API", - Template: "hello-world", - Lang: "go", - }, - { - ItemTitle: "Hello World", - Desc: "A simple REST API", - Template: "ts/hello-world", - Lang: "ts", - }, - { - ItemTitle: "Uptime Monitor", - Desc: "Microservices, SQL Databases, Pub/Sub, Cron Jobs", - Template: "uptime", - Lang: "go", - }, - { - ItemTitle: "Uptime Monitor", - Desc: "Microservices, SQL Databases, Pub/Sub, Cron Jobs", - Template: "ts/uptime", - Lang: "ts", - }, - { - ItemTitle: "GraphQL", - Desc: "GraphQL API, Microservices, SQL Database", - Template: "graphql", - Lang: "go", - }, - { - ItemTitle: "URL Shortener", - Desc: "REST API, SQL Database", - Template: "url-shortener", - Lang: "go", - }, - { - ItemTitle: "URL Shortener", - Desc: "REST API, SQL Database", - Template: "ts/url-shortener", - Lang: "ts", - }, - { - ItemTitle: "Empty app", - Desc: "Start from scratch (experienced users only)", - Template: "", - Lang: "go", - }, - { - ItemTitle: "Empty app", - Desc: "Start from scratch (experienced users only)", - Template: "ts/empty", - Lang: "ts", - }, - } - })() - - return loadedTemplates(templates) + var wg sync.WaitGroup + var templates, tutorials []templateItem + wg.Add(1) + go func() { + defer wg.Done() + templates = fetchTemplates("https://raw.githubusercontent.com/encoredev/examples/main/cli-templates.json", defaultTemplates) + }() + wg.Add(1) + go func() { + defer wg.Done() + tutorials = fetchTemplates("https://raw.githubusercontent.com/encoredev/examples/main/cli-tutorials.json", defaultTutorials) + }() + wg.Wait() + return loadedTemplates(append(tutorials, templates...)) } // incrementalValidateName is like validateName but only diff --git a/cli/cmd/encore/app/initialize.go b/cli/cmd/encore/app/initialize.go index 9f6a47b706..c9e885620d 100644 --- a/cli/cmd/encore/app/initialize.go +++ b/cli/cmd/encore/app/initialize.go @@ -73,7 +73,7 @@ func initializeApp(name string) error { cyan := color.New(color.FgCyan) promptAccountCreation() - name, _, lang, _ := selectTemplate(name, "", true) + name, _, lang := selectTemplate(name, "", true) if err := validateName(name); err != nil { return err