Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Option to create Edge Functions with JavaScript syntax #2763

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion cmd/functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ var (
},
}

flang string
functionsNewCmd = &cobra.Command{
Use: "new <Function name>",
Short: "Create a new Function locally",
Expand All @@ -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())
},
}

Expand Down Expand Up @@ -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)
Expand Down
15 changes: 13 additions & 2 deletions internal/functions/deploy/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions internal/functions/deploy/deploy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
65 changes: 58 additions & 7 deletions internal/functions/new/new.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"html/template"
"os"
"path/filepath"
"slices"
"strings"

"github.com/go-errors/errors"
"github.com/spf13/afero"
Expand All @@ -17,43 +19,92 @@ 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 {
URL string
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)
{
if err := utils.ValidateFunctionSlug(slug); err != nil {
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 {
Copy link
Contributor

@sweatybridge sweatybridge Oct 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reminder to self: support in-place config update to unblock this functions use case

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)
}
}
}

Expand Down
43 changes: 39 additions & 4 deletions internal/functions/new/new_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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))
})
}
29 changes: 29 additions & 0 deletions internal/functions/new/templates/index.js
Original file line number Diff line number Diff line change
@@ -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"}'

*/
2 changes: 1 addition & 1 deletion internal/init/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
11 changes: 8 additions & 3 deletions internal/utils/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand Down
9 changes: 5 additions & 4 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
sweatybridge marked this conversation as resolved.
Show resolved Hide resolved
}

FunctionConfig map[string]function
Expand Down
1 change: 1 addition & 0 deletions pkg/config/templates/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down