From 62a1505e7c50d61d8cb60ace64a69ca92bb48e36 Mon Sep 17 00:00:00 2001 From: Lakshan Perera Date: Thu, 10 Oct 2024 02:41:45 +1100 Subject: [PATCH 1/3] feat: support serving and deploying js functions --- internal/functions/deploy/deploy.go | 15 +++++++++++++-- internal/functions/deploy/deploy_test.go | 7 +++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/internal/functions/deploy/deploy.go b/internal/functions/deploy/deploy.go index fbed1313c..065813a04 100644 --- a/internal/functions/deploy/deploy.go +++ b/internal/functions/deploy/deploy.go @@ -50,7 +50,7 @@ func Run(ctx context.Context, slugs []string, projectRef string, noVerifyJWT *bo } func GetFunctionSlugs(fsys afero.Fs) (slugs []string, disabledSlugs []string, err error) { - pattern := filepath.Join(utils.FunctionsDir, "*", "index.ts") + pattern := filepath.Join(utils.FunctionsDir, "*", "index.[jt]s") paths, err := afero.Glob(fsys, pattern) if err != nil { return nil, nil, errors.Errorf("failed to glob function slugs: %w", err) @@ -86,7 +86,18 @@ func GetFunctionConfig(slugs []string, importMapPath string, noVerifyJWT *bool, function := utils.Config.Functions[name] // Precedence order: flag > config > fallback if len(function.Entrypoint) == 0 { - function.Entrypoint = filepath.Join(utils.FunctionsDir, name, "index.ts") + // glob for possible entrypoints + pattern := filepath.Join(utils.FunctionsDir, name, "index.[jt]s") + paths, err := afero.Glob(fsys, pattern) + if err != nil { + return nil, errors.Errorf("failed to glob function entrypoint paths: %w", err) + } + if len(paths) == 0 { + return nil, errors.Errorf("No valid index file for %s. Index file must have either .ts or .js extension", name) + } + + // use the first matching path + function.Entrypoint = paths[0] } if len(importMapPath) > 0 { function.ImportMap = importMapPath diff --git a/internal/functions/deploy/deploy_test.go b/internal/functions/deploy/deploy_test.go index adf49e63f..dadc294f4 100644 --- a/internal/functions/deploy/deploy_test.go +++ b/internal/functions/deploy/deploy_test.go @@ -28,6 +28,8 @@ func TestDeployCommand(t *testing.T) { // Setup in-memory fs fsys := afero.NewMemMapFs() require.NoError(t, utils.WriteConfig(fsys, false)) + require.NoError(t, afero.WriteFile(fsys, filepath.Join(utils.FunctionsDir, "test-func", "index.ts"), []byte("{}"), 0644)) + require.NoError(t, afero.WriteFile(fsys, filepath.Join(utils.FunctionsDir, "test-func-2", "index.js"), []byte("{}"), 0644)) // Setup valid project ref project := apitest.RandomProjectRef() // Setup valid access token @@ -290,6 +292,7 @@ func TestImportMapPath(t *testing.T) { // Setup in-memory fs fsys := afero.NewMemMapFs() require.NoError(t, afero.WriteFile(fsys, utils.FallbackImportMapPath, []byte("{}"), 0644)) + require.NoError(t, afero.WriteFile(fsys, filepath.Join(utils.FunctionsDir, "test", "index.ts"), []byte("{}"), 0644)) // Run test fc, err := GetFunctionConfig([]string{"test"}, "", nil, fsys) // Check error @@ -306,6 +309,7 @@ func TestImportMapPath(t *testing.T) { // Setup in-memory fs fsys := afero.NewMemMapFs() require.NoError(t, afero.WriteFile(fsys, utils.FallbackImportMapPath, []byte("{}"), 0644)) + require.NoError(t, afero.WriteFile(fsys, filepath.Join(utils.FunctionsDir, "hello", "index.ts"), []byte("{}"), 0644)) // Run test fc, err := GetFunctionConfig([]string{slug}, "", nil, fsys) // Check error @@ -322,6 +326,7 @@ func TestImportMapPath(t *testing.T) { // Setup in-memory fs fsys := afero.NewMemMapFs() require.NoError(t, afero.WriteFile(fsys, utils.FallbackImportMapPath, []byte("{}"), 0644)) + require.NoError(t, afero.WriteFile(fsys, filepath.Join(utils.FunctionsDir, "hello", "index.ts"), []byte("{}"), 0644)) // Run test fc, err := GetFunctionConfig([]string{slug}, utils.FallbackImportMapPath, utils.Ptr(false), fsys) // Check error @@ -332,6 +337,7 @@ func TestImportMapPath(t *testing.T) { t.Run("returns empty string if no fallback", func(t *testing.T) { // Setup in-memory fs fsys := afero.NewMemMapFs() + require.NoError(t, afero.WriteFile(fsys, filepath.Join(utils.FunctionsDir, "test", "index.ts"), []byte("{}"), 0644)) // Run test fc, err := GetFunctionConfig([]string{"test"}, "", nil, fsys) // Check error @@ -344,6 +350,7 @@ func TestImportMapPath(t *testing.T) { // Setup in-memory fs fsys := afero.NewMemMapFs() require.NoError(t, afero.WriteFile(fsys, utils.FallbackImportMapPath, []byte("{}"), 0644)) + require.NoError(t, afero.WriteFile(fsys, filepath.Join(utils.FunctionsDir, "test", "index.ts"), []byte("{}"), 0644)) // Run test fc, err := GetFunctionConfig([]string{"test"}, path, nil, fsys) // Check error From 619b994a78acbbe6298e6a5230523f9b006e6754 Mon Sep 17 00:00:00 2001 From: Lakshan Perera Date: Thu, 10 Oct 2024 16:00:44 +1100 Subject: [PATCH 2/3] fix: prompt for preferred language when creating first function --- cmd/functions.go | 4 +- internal/functions/new/new.go | 65 ++++++++++++++++++++--- internal/functions/new/new_test.go | 43 +++++++++++++-- internal/functions/new/templates/index.js | 29 ++++++++++ internal/utils/config.go | 11 ++-- pkg/config/config.go | 9 ++-- pkg/config/templates/config.toml | 1 + 7 files changed, 143 insertions(+), 19 deletions(-) create mode 100644 internal/functions/new/templates/index.js diff --git a/cmd/functions.go b/cmd/functions.go index 6555bae9b..a4816fb77 100644 --- a/cmd/functions.go +++ b/cmd/functions.go @@ -68,6 +68,7 @@ var ( }, } + flang string functionsNewCmd = &cobra.Command{ Use: "new ", Short: "Create a new Function locally", @@ -77,7 +78,7 @@ var ( return cmd.Root().PersistentPreRunE(cmd, args) }, RunE: func(cmd *cobra.Command, args []string) error { - return new_.Run(cmd.Context(), args[0], afero.NewOsFs()) + return new_.Run(cmd.Context(), args[0], flang, afero.NewOsFs()) }, } @@ -138,6 +139,7 @@ func init() { cobra.CheckErr(functionsServeCmd.Flags().MarkHidden("all")) functionsDownloadCmd.Flags().StringVar(&flags.ProjectRef, "project-ref", "", "Project ref of the Supabase project.") functionsDownloadCmd.Flags().BoolVar(&useLegacyBundle, "legacy-bundle", false, "Use legacy bundling mechanism.") + functionsNewCmd.Flags().StringVar(&flang, "lang", "", "Language of the Function.") functionsCmd.AddCommand(functionsListCmd) functionsCmd.AddCommand(functionsDeleteCmd) functionsCmd.AddCommand(functionsDeployCmd) diff --git a/internal/functions/new/new.go b/internal/functions/new/new.go index 3656e9b5f..5374f8129 100644 --- a/internal/functions/new/new.go +++ b/internal/functions/new/new.go @@ -7,6 +7,8 @@ import ( "html/template" "os" "path/filepath" + "slices" + "strings" "github.com/go-errors/errors" "github.com/spf13/afero" @@ -17,6 +19,10 @@ var ( //go:embed templates/index.ts indexEmbed string indexTemplate = template.Must(template.New("indexl").Parse(indexEmbed)) + + //go:embed templates/index.js + indexJsEmbed string + indexJsTemplate = template.Must(template.New("indexjs").Parse(indexJsEmbed)) ) type indexConfig struct { @@ -24,7 +30,7 @@ type indexConfig struct { Token string } -func Run(ctx context.Context, slug string, fsys afero.Fs) error { +func Run(ctx context.Context, slug string, lang string, fsys afero.Fs) error { // 1. Sanity checks. funcDir := filepath.Join(utils.FunctionsDir, slug) { @@ -32,28 +38,73 @@ func Run(ctx context.Context, slug string, fsys afero.Fs) error { return err } } + if err := utils.LoadConfigFS(fsys); err != nil { + utils.CmdSuggestion = "" + } + + // 2. Set preferred language in config + if lang == "" { + exists, err := afero.Exists(fsys, utils.FunctionsDir) + if err != nil { + return err + } + + if !exists { + title := "Which language you want to use for your functions?" + items := []utils.PromptItem{ + {Summary: "TypeScript"}, + {Summary: "JavaScript"}, + } + choice, err := utils.PromptChoice(ctx, title, items) + if err != nil { + return err + } + // update config + if err := utils.InitConfig(utils.InitParams{EdgeRuntimeDefaultLanguage: choice.Summary, Overwrite: true}, fsys); err != nil { + return err + } + // reload config + if err := utils.LoadConfigFS(fsys); err != nil { + return err + } + } - // 2. Create new function. + lang = utils.Config.EdgeRuntime.DefaultLanguage + } + + useJs := false + if slices.Contains([]string{"javascript", "js"}, strings.ToLower(lang)) { + useJs = true + } + + // 3. Create new function. { if err := utils.MkdirIfNotExistFS(fsys, funcDir); err != nil { return err } + path := filepath.Join(funcDir, "index.ts") + if useJs { + path = filepath.Join(funcDir, "index.js") + } f, err := fsys.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644) if err != nil { return errors.Errorf("failed to create function entrypoint: %w", err) } defer f.Close() // Templatize index.ts by config.toml if available - if err := utils.LoadConfigFS(fsys); err != nil { - utils.CmdSuggestion = "" - } config := indexConfig{ URL: utils.GetApiUrl("/functions/v1/" + slug), Token: utils.Config.Auth.AnonKey, } - if err := indexTemplate.Option("missingkey=error").Execute(f, config); err != nil { - return errors.Errorf("failed to initialise function entrypoint: %w", err) + if useJs { + if err := indexJsTemplate.Option("missingkey=error").Execute(f, config); err != nil { + return errors.Errorf("failed to initialise function entrypoint: %w", err) + } + } else { + if err := indexTemplate.Option("missingkey=error").Execute(f, config); err != nil { + return errors.Errorf("failed to initialise function entrypoint: %w", err) + } } } diff --git a/internal/functions/new/new_test.go b/internal/functions/new/new_test.go index d5e9c2fc8..529b32e32 100644 --- a/internal/functions/new/new_test.go +++ b/internal/functions/new/new_test.go @@ -15,8 +15,10 @@ func TestNewCommand(t *testing.T) { t.Run("creates new function", func(t *testing.T) { // Setup in-memory fs fsys := afero.NewMemMapFs() + require.NoError(t, fsys.MkdirAll(utils.FunctionsDir, 0755)) + require.NoError(t, utils.InitConfig(utils.InitParams{ProjectId: "test"}, fsys)) // Run test - assert.NoError(t, Run(context.Background(), "test-func", fsys)) + assert.NoError(t, Run(context.Background(), "test-func", "", fsys)) // Validate output funcPath := filepath.Join(utils.FunctionsDir, "test-func", "index.ts") content, err := afero.ReadFile(fsys, funcPath) @@ -26,23 +28,56 @@ func TestNewCommand(t *testing.T) { ) }) + t.Run("creates new JS function when lang==js flag is set", func(t *testing.T) { + // Setup in-memory fs + fsys := afero.NewMemMapFs() + require.NoError(t, utils.InitConfig(utils.InitParams{ProjectId: "test"}, fsys)) + // Run test + assert.NoError(t, Run(context.Background(), "test-func", "js", fsys)) + // Validate output + funcPath := filepath.Join(utils.FunctionsDir, "test-func", "index.js") + content, err := afero.ReadFile(fsys, funcPath) + assert.NoError(t, err) + assert.Contains(t, string(content), + "curl -i --location --request POST 'http://127.0.0.1:54321/functions/v1/test-func'", + ) + }) + + t.Run("creates new JS function when default language is set to javascript in config", func(t *testing.T) { + // Setup in-memory fs + fsys := afero.NewMemMapFs() + require.NoError(t, fsys.MkdirAll(utils.FunctionsDir, 0755)) + require.NoError(t, utils.InitConfig(utils.InitParams{ProjectId: "test", EdgeRuntimeDefaultLanguage: "javascript"}, fsys)) + + // Run test + assert.NoError(t, Run(context.Background(), "test-func", "", fsys)) + // Validate output + funcPath := filepath.Join(utils.FunctionsDir, "test-func", "index.js") + content, err := afero.ReadFile(fsys, funcPath) + assert.NoError(t, err) + assert.Contains(t, string(content), + "curl -i --location --request POST 'http://127.0.0.1:54321/functions/v1/test-func'", + ) + }) + t.Run("throws error on malformed slug", func(t *testing.T) { - assert.Error(t, Run(context.Background(), "@", afero.NewMemMapFs())) + assert.Error(t, Run(context.Background(), "@", "", afero.NewMemMapFs())) }) t.Run("throws error on duplicate slug", func(t *testing.T) { // Setup in-memory fs fsys := afero.NewMemMapFs() + require.NoError(t, utils.InitConfig(utils.InitParams{ProjectId: "test"}, fsys)) funcPath := filepath.Join(utils.FunctionsDir, "test-func", "index.ts") require.NoError(t, afero.WriteFile(fsys, funcPath, []byte{}, 0644)) // Run test - assert.Error(t, Run(context.Background(), "test-func", fsys)) + assert.Error(t, Run(context.Background(), "test-func", "", fsys)) }) t.Run("throws error on permission denied", func(t *testing.T) { // Setup in-memory fs fsys := afero.NewReadOnlyFs(afero.NewMemMapFs()) // Run test - assert.Error(t, Run(context.Background(), "test-func", fsys)) + assert.Error(t, Run(context.Background(), "test-func", "", fsys)) }) } diff --git a/internal/functions/new/templates/index.js b/internal/functions/new/templates/index.js new file mode 100644 index 000000000..fe504ddbb --- /dev/null +++ b/internal/functions/new/templates/index.js @@ -0,0 +1,29 @@ +// Follow this setup guide to integrate the Deno language server with your editor: +// https://deno.land/manual/getting_started/setup_your_environment +// This enables autocomplete, go to definition, etc. + +console.log("Hello from Functions!"); + +Deno.serve(async (req) => { + const { name } = await req.json(); + const data = { + message: `Hello ${name}!`, + }; + + return new Response( + JSON.stringify(data), + { headers: { "Content-Type": "application/json" } }, + ); +}); + +/* To invoke locally: + + 1. Run `supabase start` (see: https://supabase.com/docs/reference/cli/supabase-start) + 2. Make an HTTP request: + + curl -i --location --request POST '{{ .URL }}' \ + --header 'Authorization: Bearer {{ .Token }}' \ + --header 'Content-Type: application/json' \ + --data '{"name":"Functions"}' + +*/ diff --git a/internal/utils/config.go b/internal/utils/config.go index 3f89dc75c..92c72fab3 100644 --- a/internal/utils/config.go +++ b/internal/utils/config.go @@ -133,9 +133,10 @@ func ToRealtimeEnv(addr config.AddressFamily) string { } type InitParams struct { - ProjectId string - UseOrioleDB bool - Overwrite bool + ProjectId string + UseOrioleDB bool + EdgeRuntimeDefaultLanguage string + Overwrite bool } func InitConfig(params InitParams, fsys afero.Fs) error { @@ -144,6 +145,10 @@ func InitConfig(params InitParams, fsys afero.Fs) error { if params.UseOrioleDB { c.Experimental.OrioleDBVersion = "15.1.0.150" } + c.EdgeRuntime.DefaultLanguage = "typescript" + if params.EdgeRuntimeDefaultLanguage != "" { + c.EdgeRuntime.DefaultLanguage = params.EdgeRuntimeDefaultLanguage + } // Create config file if err := MkdirIfNotExistFS(fsys, SupabaseDirPath); err != nil { return err diff --git a/pkg/config/config.go b/pkg/config/config.go index 5fbd4661f..4bc953100 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -412,10 +412,11 @@ type ( } edgeRuntime struct { - Enabled bool `toml:"enabled"` - Image string `toml:"-"` - Policy RequestPolicy `toml:"policy"` - InspectorPort uint16 `toml:"inspector_port"` + Enabled bool `toml:"enabled"` + Image string `toml:"-"` + Policy RequestPolicy `toml:"policy"` + InspectorPort uint16 `toml:"inspector_port"` + DefaultLanguage string `toml:"default_language"` } FunctionConfig map[string]function diff --git a/pkg/config/templates/config.toml b/pkg/config/templates/config.toml index 112237748..dcf918f35 100644 --- a/pkg/config/templates/config.toml +++ b/pkg/config/templates/config.toml @@ -226,6 +226,7 @@ enabled = true # Use `oneshot` for hot reload, or `per_worker` for load testing. policy = "oneshot" inspector_port = 8083 +default_language = "{{ .EdgeRuntime.DefaultLanguage }}" [analytics] enabled = true From fa9f71e4041bc727abe75e14f625302ab800386a Mon Sep 17 00:00:00 2001 From: Lakshan Perera Date: Mon, 14 Oct 2024 10:56:18 +1100 Subject: [PATCH 3/3] chore: explainer for vs code settings prompt --- internal/init/init.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/init/init.go b/internal/init/init.go index f4e470b02..8734559c3 100644 --- a/internal/init/init.go +++ b/internal/init/init.go @@ -58,7 +58,7 @@ func Run(ctx context.Context, fsys afero.Fs, createVscodeSettings, createIntelli } } else { console := utils.NewConsole() - if isVscode, err := console.PromptYesNo(ctx, "Generate VS Code settings for Deno?", false); err != nil { + if isVscode, err := console.PromptYesNo(ctx, "Generate VS Code settings for Deno (recommended if you plan to write Edge Functions)?", false); err != nil { return err } else if isVscode { return writeVscodeConfig(fsys)