Skip to content
Open
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
16 changes: 12 additions & 4 deletions cmd/functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/supabase/cli/internal/utils"
"github.com/supabase/cli/internal/utils/flags"
"github.com/supabase/cli/pkg/cast"
"github.com/supabase/cli/pkg/config"
)

var (
Expand Down Expand Up @@ -106,13 +107,20 @@ var (
runtimeOption serve.RuntimeOption

functionsServeCmd = &cobra.Command{
Use: "serve",
Short: "Serve all Functions locally",
Use: "serve [Function name]",
Short: "Serve Functions locally",
Long: "Serve Functions locally, omit function names to serve all Functions.",
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
cmd.GroupID = groupLocalDev
return cmd.Root().PersistentPreRunE(cmd, args)
},
RunE: func(cmd *cobra.Command, args []string) error {
for _, slug := range args {
if err := config.ValidateFunctionSlug(slug); err != nil {
return err
}
}

// Fallback to config if user did not set the flag.
if !cmd.Flags().Changed("no-verify-jwt") {
noVerifyJWT = nil
Expand All @@ -127,7 +135,7 @@ var (
return fmt.Errorf("--inspect-main must be used together with one of these flags: [inspect inspect-mode]")
}

return serve.Run(cmd.Context(), envFilePath, noVerifyJWT, importMapPath, runtimeOption, afero.NewOsFs())
return serve.Run(cmd.Context(), args, envFilePath, noVerifyJWT, importMapPath, runtimeOption, afero.NewOsFs())
},
}
)
Expand All @@ -154,7 +162,7 @@ func init() {
functionsServeCmd.Flags().Var(&inspectMode, "inspect-mode", "Activate inspector capability for debugging.")
functionsServeCmd.Flags().BoolVar(&runtimeOption.InspectMain, "inspect-main", false, "Allow inspecting the main worker.")
functionsServeCmd.MarkFlagsMutuallyExclusive("inspect", "inspect-mode")
functionsServeCmd.Flags().Bool("all", true, "Serve all Functions.")
functionsServeCmd.Flags().Bool("all", true, "Serve all Functions.") // TODO: maybe remove this flag in next major release? it currently does nothing
cobra.CheckErr(functionsServeCmd.Flags().MarkHidden("all"))
downloadFlags := functionsDownloadCmd.Flags()
downloadFlags.StringVar(&flags.ProjectRef, "project-ref", "", "Project ref of the Supabase project.")
Expand Down
4 changes: 3 additions & 1 deletion docs/supabase/functions/serve.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
## supabase-functions-serve

Serve all Functions locally.
Serve Functions locally.

When no function names are provided, all functions in config.toml file plus those in the `supabase/functions` directory are served. When one or more function names are specified, only those functions will be served.

`supabase functions serve` command includes additional flags to assist developers in debugging Edge Functions via the v8 inspector protocol, allowing for debugging via Chrome DevTools, VS Code, and IntelliJ IDEA for example. Refer to the [docs guide](/docs/guides/functions/debugging-tools) for setup instructions.

Expand Down
25 changes: 14 additions & 11 deletions internal/functions/serve/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ const (
//go:embed templates/main.ts
var mainFuncEmbed string

func Run(ctx context.Context, envFilePath string, noVerifyJWT *bool, importMapPath string, runtimeOption RuntimeOption, fsys afero.Fs) error {
func Run(ctx context.Context, slugs []string, envFilePath string, noVerifyJWT *bool, importMapPath string, runtimeOption RuntimeOption, fsys afero.Fs) error {
watcher, err := NewDebounceFileWatcher()
if err != nil {
return err
Expand All @@ -79,7 +79,7 @@ func Run(ctx context.Context, envFilePath string, noVerifyJWT *bool, importMapPa
defer watcher.Close()
// TODO: refactor this to edge runtime service
runtimeOption.fileWatcher = watcher
if err := restartEdgeRuntime(ctx, envFilePath, noVerifyJWT, importMapPath, runtimeOption, fsys); err != nil {
if err := restartEdgeRuntime(ctx, slugs, envFilePath, noVerifyJWT, importMapPath, runtimeOption, fsys); err != nil {
return err
}
streamer := NewLogStreamer(ctx)
Expand All @@ -91,7 +91,7 @@ func Run(ctx context.Context, envFilePath string, noVerifyJWT *bool, importMapPa
fmt.Println("Stopped serving " + utils.Bold(utils.FunctionsDir))
return ctx.Err()
case <-watcher.RestartCh:
if err := restartEdgeRuntime(ctx, envFilePath, noVerifyJWT, importMapPath, runtimeOption, fsys); err != nil {
if err := restartEdgeRuntime(ctx, slugs, envFilePath, noVerifyJWT, importMapPath, runtimeOption, fsys); err != nil {
return err
}
case err := <-streamer.ErrCh:
Expand All @@ -100,7 +100,7 @@ func Run(ctx context.Context, envFilePath string, noVerifyJWT *bool, importMapPa
}
}

func restartEdgeRuntime(ctx context.Context, envFilePath string, noVerifyJWT *bool, importMapPath string, runtimeOption RuntimeOption, fsys afero.Fs) error {
func restartEdgeRuntime(ctx context.Context, slugs []string, envFilePath string, noVerifyJWT *bool, importMapPath string, runtimeOption RuntimeOption, fsys afero.Fs) error {
// 1. Sanity checks.
if err := flags.LoadConfig(fsys); err != nil {
return err
Expand All @@ -117,10 +117,10 @@ func restartEdgeRuntime(ctx context.Context, envFilePath string, noVerifyJWT *bo
dbUrl := fmt.Sprintf("postgresql://postgres:postgres@%s:5432/postgres", utils.DbAliases[0])
// 3. Serve and log to console
fmt.Fprintln(os.Stderr, "Setting up Edge Functions runtime...")
return ServeFunctions(ctx, envFilePath, noVerifyJWT, importMapPath, dbUrl, runtimeOption, fsys)
return ServeFunctions(ctx, slugs, envFilePath, noVerifyJWT, importMapPath, dbUrl, runtimeOption, fsys)
}

func ServeFunctions(ctx context.Context, envFilePath string, noVerifyJWT *bool, importMapPath string, dbUrl string, runtimeOption RuntimeOption, fsys afero.Fs) error {
func ServeFunctions(ctx context.Context, slugs []string, envFilePath string, noVerifyJWT *bool, importMapPath string, dbUrl string, runtimeOption RuntimeOption, fsys afero.Fs) error {
// 1. Parse custom env file
env, err := parseEnvFile(envFilePath, fsys)
if err != nil {
Expand Down Expand Up @@ -153,7 +153,7 @@ func ServeFunctions(ctx context.Context, envFilePath string, noVerifyJWT *bool,
return errors.Errorf("failed to resolve relative path: %w", err)
}
}
binds, functionsConfigString, err := PopulatePerFunctionConfigs(cwd, importMapPath, noVerifyJWT, fsys)
binds, functionsConfigString, err := PopulatePerFunctionConfigs(slugs, cwd, importMapPath, noVerifyJWT, fsys)
if err != nil {
return err
}
Expand Down Expand Up @@ -241,10 +241,13 @@ func parseEnvFile(envFilePath string, fsys afero.Fs) ([]string, error) {
return env, err
}

func PopulatePerFunctionConfigs(cwd, importMapPath string, noVerifyJWT *bool, fsys afero.Fs) ([]string, string, error) {
slugs, err := deploy.GetFunctionSlugs(fsys)
if err != nil {
return nil, "", err
func PopulatePerFunctionConfigs(slugs []string, cwd, importMapPath string, noVerifyJWT *bool, fsys afero.Fs) ([]string, string, error) {
var err error
if len(slugs) == 0 {
slugs, err = deploy.GetFunctionSlugs(fsys)
if err != nil {
return nil, "", err
}
}
functionsConfig, err := deploy.GetFunctionConfig(slugs, importMapPath, noVerifyJWT, fsys)
if err != nil {
Expand Down
77 changes: 67 additions & 10 deletions internal/functions/serve/serve_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ func TestServeCommand(t *testing.T) {
apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.EdgeRuntime.Image), containerId)
require.NoError(t, apitest.MockDockerLogsStream(utils.Docker, containerId, 1, strings.NewReader("failed")))
// Run test with timeout context
err := Run(context.Background(), "", nil, "", RuntimeOption{}, fsys)
err := Run(context.Background(), nil, "", nil, "", RuntimeOption{}, fsys)
// Check error
assert.ErrorContains(t, err, "error running container: exit 1")
assert.Empty(t, apitest.ListUnmatchedRequests())
Expand All @@ -57,7 +57,7 @@ func TestServeCommand(t *testing.T) {
fsys := afero.NewMemMapFs()
require.NoError(t, afero.WriteFile(fsys, utils.ConfigPath, []byte("malformed"), 0644))
// Run test
err := Run(context.Background(), "", nil, "", RuntimeOption{}, fsys)
err := Run(context.Background(), nil, "", nil, "", RuntimeOption{}, fsys)
// Check error
assert.ErrorContains(t, err, "toml: expected = after a key, but the document ends there")
})
Expand All @@ -73,7 +73,7 @@ func TestServeCommand(t *testing.T) {
Get("/v" + utils.Docker.ClientVersion() + "/containers/supabase_db_test/json").
Reply(http.StatusNotFound)
// Run test
err := Run(context.Background(), "", nil, "", RuntimeOption{}, fsys)
err := Run(context.Background(), nil, "", nil, "", RuntimeOption{}, fsys)
// Check error
assert.ErrorIs(t, err, utils.ErrNotRunning)
})
Expand All @@ -90,7 +90,7 @@ func TestServeCommand(t *testing.T) {
Reply(http.StatusOK).
JSON(container.InspectResponse{})
// Run test
err := Run(context.Background(), ".env", nil, "", RuntimeOption{}, fsys)
err := Run(context.Background(), nil, ".env", nil, "", RuntimeOption{}, fsys)
// Check error
assert.ErrorContains(t, err, "open .env: file does not exist")
})
Expand All @@ -110,7 +110,7 @@ func TestServeCommand(t *testing.T) {
Reply(http.StatusOK).
JSON(container.InspectResponse{})
// Run test
err := Run(context.Background(), ".env", cast.Ptr(true), "import_map.json", RuntimeOption{}, fsys)
err := Run(context.Background(), nil, ".env", cast.Ptr(true), "import_map.json", RuntimeOption{}, fsys)
// Check error
assert.ErrorContains(t, err, "failed to resolve relative path:")
})
Expand All @@ -128,7 +128,7 @@ func TestServeFunctions(t *testing.T) {
defer gock.OffAll()
apitest.MockDockerStart(utils.Docker, utils.GetRegistryImageUrl(utils.Config.EdgeRuntime.Image), utils.EdgeRuntimeId)
// Run test
err := ServeFunctions(context.Background(), "", nil, "", "", RuntimeOption{
err := ServeFunctions(context.Background(), nil, "", nil, "", "", RuntimeOption{
InspectMode: cast.Ptr(InspectModeRun),
InspectMain: true,
}, fsys)
Expand Down Expand Up @@ -157,17 +157,74 @@ func TestServeFunctions(t *testing.T) {
}, env)
})

t.Run("parses function config", func(t *testing.T) {
t.Run("parses function config for all functions", func(t *testing.T) {
// Setup in-memory fs
fsys := afero.FromIOFS{FS: testdata}
// Run test
binds, configString, err := PopulatePerFunctionConfigs("/", "", nil, fsys)
// Run test with nil slugs (serve all)
binds, configString, err := PopulatePerFunctionConfigs(nil, "/", "", nil, fsys)
// Check error
assert.NoError(t, err)
assert.ElementsMatch(t, []string{
"supabase_edge_runtime_test:/root/.cache/deno:rw",
"/supabase/functions/:/supabase/functions/:ro",
}, binds)
assert.Equal(t, `{"hello":{"verifyJWT":true,"entrypointPath":"testdata/functions/hello/index.ts","staticFiles":["testdata/image.png"]}}`, configString)
// Should contain hello, good, bye but NOT world (disabled)
assert.Contains(t, configString, `"hello"`)
assert.Contains(t, configString, `"good"`)
assert.Contains(t, configString, `"bye"`)
assert.NotContains(t, configString, `"world"`)
})

t.Run("serves only disabled function returns empty config", func(t *testing.T) {
// Setup in-memory fs
fsys := afero.FromIOFS{FS: testdata}
// Run test with only disabled function
_, configString, err := PopulatePerFunctionConfigs([]string{"world"}, "/", "", nil, fsys)
// Check error
assert.NoError(t, err)
// Config should be empty since world is disabled
assert.Equal(t, "{}", configString)
})

t.Run("serves single specific function", func(t *testing.T) {
// Setup in-memory fs
fsys := afero.FromIOFS{FS: testdata}
// Run test with single slug
_, configString, err := PopulatePerFunctionConfigs([]string{"hello"}, "/", "", nil, fsys)
// Check error
assert.NoError(t, err)
// Should only contain hello
assert.Contains(t, configString, `"hello"`)
assert.NotContains(t, configString, `"good"`)
assert.NotContains(t, configString, `"bye"`)
assert.NotContains(t, configString, `"world"`)
})

t.Run("serves multiple specific enabled functions", func(t *testing.T) {
// Setup in-memory fs
fsys := afero.FromIOFS{FS: testdata}
// Run test with multiple enabled slugs
_, configString, err := PopulatePerFunctionConfigs([]string{"hello", "good", "bye"}, "/", "", nil, fsys)
// Check error
assert.NoError(t, err)
// Should contain all three
assert.Contains(t, configString, `"hello"`)
assert.Contains(t, configString, `"good"`)
assert.Contains(t, configString, `"bye"`)
assert.NotContains(t, configString, `"world"`)
})

t.Run("serves multiple functions skipping disabled ones", func(t *testing.T) {
// Setup in-memory fs
fsys := afero.FromIOFS{FS: testdata}
// Run test with mix of enabled and disabled slugs
_, configString, err := PopulatePerFunctionConfigs([]string{"hello", "world", "good", "bye"}, "/", "", nil, fsys)
// Check error
assert.NoError(t, err)
// Should contain hello, good, bye but NOT world (disabled)
assert.Contains(t, configString, `"hello"`)
assert.Contains(t, configString, `"good"`)
assert.Contains(t, configString, `"bye"`)
assert.NotContains(t, configString, `"world"`)
})
}
4 changes: 4 additions & 0 deletions internal/functions/serve/testdata/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,7 @@ static_files = ["image.png"]
[functions.world]
enabled = false
verify_jwt = false

[functions.good]

[functions.bye]
4 changes: 2 additions & 2 deletions internal/start/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -1094,7 +1094,7 @@ EOF
// Start all functions.
if utils.Config.EdgeRuntime.Enabled && !isContainerExcluded(utils.Config.EdgeRuntime.Image, excluded) {
dbUrl := fmt.Sprintf("postgresql://%s:%s@%s:%d/%s", dbConfig.User, dbConfig.Password, dbConfig.Host, dbConfig.Port, dbConfig.Database)
if err := serve.ServeFunctions(ctx, "", nil, "", dbUrl, serve.RuntimeOption{}, fsys); err != nil {
if err := serve.ServeFunctions(ctx, nil, "", nil, "", dbUrl, serve.RuntimeOption{}, fsys); err != nil {
return err
}
started = append(started, utils.EdgeRuntimeId)
Expand Down Expand Up @@ -1140,7 +1140,7 @@ EOF

// Start Studio.
if utils.Config.Studio.Enabled && !isContainerExcluded(utils.Config.Studio.Image, excluded) {
binds, _, err := serve.PopulatePerFunctionConfigs(workdir, "", nil, fsys)
binds, _, err := serve.PopulatePerFunctionConfigs(nil, workdir, "", nil, fsys)
if err != nil {
return err
}
Expand Down
Loading