diff --git a/internal/functions/deploy/deploy.go b/internal/functions/deploy/deploy.go index 33ba5c23c..315936c87 100644 --- a/internal/functions/deploy/deploy.go +++ b/internal/functions/deploy/deploy.go @@ -54,7 +54,7 @@ func Run(ctx context.Context, slugs []string, useDocker bool, noVerifyJWT *bool, } func GetFunctionSlugs(fsys afero.Fs) (slugs []string, err error) { - pattern := filepath.Join(utils.FunctionsDir, "*", "index.ts") + pattern := filepath.Join(utils.FunctionsDir, "*", "*.ts") paths, err := afero.Glob(fsys, pattern) if err != nil { return nil, errors.Errorf("failed to glob function slugs: %w", err) @@ -97,7 +97,15 @@ func GetFunctionConfig(slugs []string, importMapPath string, noVerifyJWT *bool, // Precedence order: flag > config > fallback functionDir := filepath.Join(utils.FunctionsDir, name) if len(function.Entrypoint) == 0 { - function.Entrypoint = filepath.Join(functionDir, "index.ts") + indexEntrypoint := filepath.Join(functionDir, "index.ts") + mainEntrypoint := filepath.Join(functionDir, "main.ts") + if _, err := fsys.Stat(indexEntrypoint); err == nil { + function.Entrypoint = indexEntrypoint + } else if _, err := fsys.Stat(mainEntrypoint); err == nil { + function.Entrypoint = mainEntrypoint + } else { + return nil, errors.Errorf("Cannot find a valid entrypoint file (index.ts or main.ts) for the '%s' function. Set the custom entrypoint path in config.toml", name) + } } if len(importMapPath) > 0 { function.ImportMap = importMapPath diff --git a/internal/functions/deploy/deploy_test.go b/internal/functions/deploy/deploy_test.go index 9a5b99785..30b6e08d4 100644 --- a/internal/functions/deploy/deploy_test.go +++ b/internal/functions/deploy/deploy_test.go @@ -31,6 +31,10 @@ 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, slug, "index.ts"), []byte{}, 0644)) + require.NoError(t, afero.WriteFile(fsys, filepath.Join(utils.FunctionsDir, slug+"-2", "index.ts"), []byte{}, 0644)) + // Setup valid access token token := apitest.RandomAccessToken(t) t.Setenv("SUPABASE_ACCESS_TOKEN", string(token)) @@ -120,6 +124,47 @@ import_map = "./import_map.json" assert.Empty(t, apitest.ListUnmatchedRequests()) }) + t.Run("deploys functions with main.ts as entrypoint", func(t *testing.T) { + t.Cleanup(func() { clear(utils.Config.Functions) }) + // Setup in-memory fs + fsys := afero.NewMemMapFs() + + // Setup function entrypoint + entrypointPath := filepath.Join(utils.FunctionsDir, slug, "main.ts") + require.NoError(t, afero.WriteFile(fsys, entrypointPath, []byte{}, 0644)) + + // Setup valid access token + token := apitest.RandomAccessToken(t) + t.Setenv("SUPABASE_ACCESS_TOKEN", string(token)) + // Setup valid deno path + _, err := fsys.Create(utils.DenoPathOverride) + require.NoError(t, err) + // Setup mock api + defer gock.OffAll() + gock.New(utils.DefaultApiHost). + Get("/v1/projects/" + flags.ProjectRef + "/functions"). + Reply(http.StatusOK). + JSON([]api.FunctionResponse{}) + gock.New(utils.DefaultApiHost). + Post("/v1/projects/"+flags.ProjectRef+"/functions"). + MatchParam("slug", slug). + ParamPresent("import_map_path"). + Reply(http.StatusCreated). + JSON(api.FunctionResponse{Id: "1"}) + // Setup mock docker + require.NoError(t, apitest.MockDocker(utils.Docker)) + apitest.MockDockerStart(utils.Docker, imageUrl, containerId) + require.NoError(t, apitest.MockDockerLogs(utils.Docker, containerId, "bundled")) + // Setup output file + outputDir := filepath.Join(utils.TempDir, fmt.Sprintf(".output_%s", slug)) + require.NoError(t, afero.WriteFile(fsys, filepath.Join(outputDir, "output.eszip"), []byte(""), 0644)) + // Run test + err = Run(context.Background(), []string{slug}, true, nil, "", 1, fsys) + // Check error + assert.NoError(t, err) + assert.Empty(t, apitest.ListUnmatchedRequests()) + }) + t.Run("skip disabled functions from config", func(t *testing.T) { t.Cleanup(func() { clear(utils.Config.Functions) }) // Setup in-memory fs @@ -190,6 +235,47 @@ import_map = "./import_map.json" assert.ErrorContains(t, err, "No Functions specified or found in supabase/functions") }) + t.Run("throws error on missing entrypoint", func(t *testing.T) { + t.Cleanup(func() { clear(utils.Config.Functions) }) + // Setup in-memory fs + fsys := afero.NewMemMapFs() + require.NoError(t, utils.WriteConfig(fsys, false)) + + // Setup function entrypoint + entrypointPath := filepath.Join(utils.FunctionsDir, slug, "not-entrypoint.ts") + require.NoError(t, afero.WriteFile(fsys, entrypointPath, []byte{}, 0644)) + + // Setup valid access token + token := apitest.RandomAccessToken(t) + t.Setenv("SUPABASE_ACCESS_TOKEN", string(token)) + // Setup valid deno path + _, err := fsys.Create(utils.DenoPathOverride) + require.NoError(t, err) + // Setup mock api + defer gock.OffAll() + gock.New(utils.DefaultApiHost). + Get("/v1/projects/" + flags.ProjectRef + "/functions"). + Reply(http.StatusOK). + JSON([]api.FunctionResponse{}) + gock.New(utils.DefaultApiHost). + Post("/v1/projects/"+flags.ProjectRef+"/functions"). + MatchParam("slug", slug). + ParamPresent("import_map_path"). + Reply(http.StatusCreated). + JSON(api.FunctionResponse{Id: "1"}) + // Setup mock docker + require.NoError(t, apitest.MockDocker(utils.Docker)) + apitest.MockDockerStart(utils.Docker, imageUrl, containerId) + require.NoError(t, apitest.MockDockerLogs(utils.Docker, containerId, "bundled")) + // Setup output file + outputDir := filepath.Join(utils.TempDir, fmt.Sprintf(".output_%s", slug)) + require.NoError(t, afero.WriteFile(fsys, filepath.Join(outputDir, "output.eszip"), []byte(""), 0644)) + // Run test + err = Run(context.Background(), []string{slug}, true, nil, "", 1, fsys) + // Check error + assert.ErrorContains(t, err, "Cannot find a valid entrypoint file") + }) + t.Run("verify_jwt param falls back to config", func(t *testing.T) { t.Cleanup(func() { clear(utils.Config.Functions) }) // Setup in-memory fs @@ -203,6 +289,10 @@ verify_jwt = false `) require.NoError(t, err) require.NoError(t, f.Close()) + + // Setup function entrypoint + require.NoError(t, afero.WriteFile(fsys, filepath.Join(utils.FunctionsDir, slug, "index.ts"), []byte{}, 0644)) + // Setup valid access token token := apitest.RandomAccessToken(t) t.Setenv("SUPABASE_ACCESS_TOKEN", string(token)) @@ -246,6 +336,10 @@ verify_jwt = false `) require.NoError(t, err) require.NoError(t, f.Close()) + + // Setup function entrypoint + require.NoError(t, afero.WriteFile(fsys, filepath.Join(utils.FunctionsDir, slug, "index.ts"), []byte{}, 0644)) + // Setup valid access token token := apitest.RandomAccessToken(t) t.Setenv("SUPABASE_ACCESS_TOKEN", string(token)) @@ -283,6 +377,10 @@ func TestImportMapPath(t *testing.T) { // Setup in-memory fs fsys := afero.NewMemMapFs() require.NoError(t, afero.WriteFile(fsys, utils.FallbackImportMapPath, []byte("{}"), 0644)) + + // Write function entrypoints + 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 @@ -299,6 +397,10 @@ func TestImportMapPath(t *testing.T) { // Setup in-memory fs fsys := afero.NewMemMapFs() require.NoError(t, afero.WriteFile(fsys, utils.FallbackImportMapPath, []byte("{}"), 0644)) + + // Write function entrypoints + require.NoError(t, afero.WriteFile(fsys, filepath.Join(utils.FunctionsDir, slug, "index.ts"), []byte{}, 0644)) + // Run test fc, err := GetFunctionConfig([]string{slug}, "", nil, fsys) // Check error @@ -319,6 +421,9 @@ func TestImportMapPath(t *testing.T) { require.NoError(t, afero.WriteFile(fsys, customImportMapPath, []byte("{}"), 0644)) // Create fallback import map to test precedence order require.NoError(t, afero.WriteFile(fsys, utils.FallbackImportMapPath, []byte("{}"), 0644)) + // Write function entrypoints + require.NoError(t, afero.WriteFile(fsys, filepath.Join(utils.FunctionsDir, slug, "index.ts"), []byte{}, 0644)) + // Run test fc, err := GetFunctionConfig([]string{slug}, customImportMapPath, cast.Ptr(false), fsys) // Check error @@ -329,6 +434,10 @@ func TestImportMapPath(t *testing.T) { t.Run("returns empty string if no fallback", func(t *testing.T) { // Setup in-memory fs fsys := afero.NewMemMapFs() + + // Write function entrypoints + 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 @@ -341,6 +450,10 @@ func TestImportMapPath(t *testing.T) { // Setup in-memory fs fsys := afero.NewMemMapFs() require.NoError(t, afero.WriteFile(fsys, utils.FallbackImportMapPath, []byte("{}"), 0644)) + + // Write function entrypoints + 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 diff --git a/pkg/config/config.go b/pkg/config/config.go index 306256806..cb2d334b0 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -654,7 +654,13 @@ func (c *baseConfig) resolve(builder pathBuilder, fsys fs.FS) error { // Resolve functions config for slug, function := range c.Functions { if len(function.Entrypoint) == 0 { - function.Entrypoint = filepath.Join(builder.FunctionsDir, slug, "index.ts") + indexEntrypoint := filepath.Join(builder.FunctionsDir, slug, "index.ts") + mainEntrypoint := filepath.Join(builder.FunctionsDir, slug, "main.ts") + if _, err := fs.Stat(fsys, indexEntrypoint); err == nil { + function.Entrypoint = indexEntrypoint + } else if _, err := fs.Stat(fsys, mainEntrypoint); err == nil { + function.Entrypoint = mainEntrypoint + } } else if !filepath.IsAbs(function.Entrypoint) { // Append supabase/ because paths in configs are specified relative to config.toml function.Entrypoint = filepath.Join(builder.SupabaseDirPath, function.Entrypoint) diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index c2f695842..8017799c2 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -384,6 +384,54 @@ func TestLoadEnv(t *testing.T) { assert.Equal(t, "test-root-key", config.Db.RootKey) } +func TestLoadFunctionEntrypoint(t *testing.T) { + t.Run("uses custom entrypoint when defined", func(t *testing.T) { + config := NewConfig() + fsys := fs.MapFS{ + "supabase/config.toml": &fs.MapFile{Data: []byte(` + project_id = "bvikqvbczudanvggcord" + [functions.hello] + entrypoint = "./functions/hello/foo.ts" + `)}, + "supabase/functions/hello/foo.ts": &fs.MapFile{}, + } + // Run test + assert.NoError(t, config.Load("", fsys)) + // Check that deno.json was set as import map + assert.Equal(t, "supabase/functions/hello/foo.ts", config.Functions["hello"].Entrypoint) + }) + + t.Run("set index.ts as the entrypoint when file is present and no explicit entrypoint set", func(t *testing.T) { + config := NewConfig() + fsys := fs.MapFS{ + "supabase/config.toml": &fs.MapFile{Data: []byte(` + project_id = "bvikqvbczudanvggcord" + [functions.hello] + `)}, + "supabase/functions/hello/index.ts": &fs.MapFile{}, + } + // Run test + assert.NoError(t, config.Load("", fsys)) + // Check that deno.json was set as import map + assert.Equal(t, "supabase/functions/hello/index.ts", config.Functions["hello"].Entrypoint) + }) + + t.Run("set main.ts as the entrypoint when file is present and no explicit entrypoint set", func(t *testing.T) { + config := NewConfig() + fsys := fs.MapFS{ + "supabase/config.toml": &fs.MapFile{Data: []byte(` + project_id = "bvikqvbczudanvggcord" + [functions.hello] + `)}, + "supabase/functions/hello/main.ts": &fs.MapFile{}, + } + // Run test + assert.NoError(t, config.Load("", fsys)) + // Check that deno.json was set as import map + assert.Equal(t, "supabase/functions/hello/main.ts", config.Functions["hello"].Entrypoint) + }) +} + func TestLoadFunctionImportMap(t *testing.T) { t.Run("uses deno.json as import map when present", func(t *testing.T) { config := NewConfig()