Repository: supabase/cli
Pull request #1103 (2023-05-15 18:19:09)

feat: support deploying functions that imports from parent directory
Reverts supabase/cli#1102

draft PR to continue testing https://github.com/supabase/cli/issues/1028

blocked on upstream changes
- [x] https://github.com/supabase/functions/pull/56
- [x] https://github.com/supabase/infrastructure/pull/12917

TODO:
- [x] update download / extract.ts
- [x] wait for api prod release

What's new
---

- `functions deploy`
  - drops legacy json bundle support by defaulting to eszip
  - always sets the `entrypointUrl` and `importMapUrl` for upstream server
- `functions download`
  - passes in the `entrypointPath` so that files are extracted relative to dest
  - tested that `download` works for functions both with and without explicit entrypoint
  - logs extracted file paths to stdout
- `functions delete`
  - no need to get function first as delete will return 404 on missing function
- `functions serve`
  - refactored `import-map` flag parsing so it's reused by deploy command


coveralls: ## Pull Request Test Coverage Report for [Build 4983784902](https://coveralls.io/builds/59967833)

* **47** of **57**   **(82.46%)**  changed or added relevant lines in **4** files are covered.
* **1** unchanged line in **1** file lost coverage.
* Overall coverage increased (+**0.2%**) to **62.278%**

---

|  Changes Missing Coverage | Covered Lines | Changed/Added Lines | % |
| :-----|--------------|--------|---: |
| [cmd/functions.go](https://coveralls.io/builds/59967833/source?filename=cmd%2Ffunctions.go#L60) | 1 | 2 | 50.0%
| [internal/utils/deno.go](https://coveralls.io/builds/59967833/source?filename=internal%2Futils%2Fdeno.go#L339) | 20 | 22 | 90.91%
| [internal/functions/deploy/deploy.go](https://coveralls.io/builds/59967833/source?filename=internal%2Ffunctions%2Fdeploy%2Fdeploy.go#L37) | 24 | 31 | 77.42%
<!-- | **Total:** | **47** | **57** | **82.46%** | -->

|  Files with Coverage Reduction | New Missed Lines | % |
| :-----|--------------|--: |
| [internal/functions/deploy/deploy.go](https://coveralls.io/builds/59967833/source?filename=internal%2Ffunctions%2Fdeploy%2Fdeploy.go#L38) | 1 | 90.7% |
<!-- | **Total:** | **1** |  | -->

|  Totals | [![Coverage Status](https://coveralls.io/builds/59967833/badge)](https://coveralls.io/builds/59967833) |
| :-- | --: |
| Change from base [Build 4983778405](https://coveralls.io/builds/59967816): |  0.2% |
| Covered Lines: | 4418 |
| Relevant Lines: | 7094 |

---
##### 💛  - [Coveralls](https://coveralls.io)


Patch:
diff --git a/api/beta.yaml b/api/beta.yaml
index a6b28913..33ebfa91 100644
--- a/api/beta.yaml
+++ b/api/beta.yaml
@@ -271,6 +271,16 @@ paths:
           in: query
           schema:
             type: boolean
+        - name: entrypoint_path
+          required: false
+          in: query
+          schema:
+            type: string
+        - name: import_map_path
+          required: false
+          in: query
+          schema:
+            type: string
       requestBody:
         required: true
         content:
@@ -402,6 +412,16 @@ paths:
           in: query
           schema:
             type: boolean
+        - name: entrypoint_path
+          required: false
+          in: query
+          schema:
+            type: string
+        - name: import_map_path
+          required: false
+          in: query
+          schema:
+            type: string
       requestBody:
         required: true
         content:
@@ -1758,6 +1778,10 @@ components:
           type: boolean
         import_map:
           type: boolean
+        entrypoint_path:
+          type: string
+        import_map_path:
+          type: string
       required:
         - id
         - slug
@@ -1788,6 +1812,10 @@ components:
           type: boolean
         import_map:
           type: boolean
+        entrypoint_path:
+          type: string
+        import_map_path:
+          type: string
       required:
         - id
         - slug
diff --git a/cmd/functions.go b/cmd/functions.go
index 4fb6f86a..129e54de 100644
--- a/cmd/functions.go
+++ b/cmd/functions.go
@@ -57,7 +57,7 @@ var (
 			if !cmd.Flags().Changed("no-verify-jwt") {
 				noVerifyJWT = nil
 			}
-			return deploy.Run(cmd.Context(), args[0], flags.ProjectRef, noVerifyJWT, useLegacyBundle, importMapPath, afero.NewOsFs())
+			return deploy.Run(cmd.Context(), args[0], flags.ProjectRef, noVerifyJWT, importMapPath, afero.NewOsFs())
 		},
 	}
 
@@ -107,6 +107,7 @@ func init() {
 	functionsDeployCmd.Flags().StringVar(&flags.ProjectRef, "project-ref", "", "Project ref of the Supabase project.")
 	functionsDeployCmd.Flags().BoolVar(&useLegacyBundle, "legacy-bundle", false, "Use legacy bundling mechanism.")
 	functionsDeployCmd.Flags().StringVar(&importMapPath, "import-map", "", "Path to import map file.")
+	cobra.CheckErr(functionsDeployCmd.Flags().MarkHidden("legacy-bundle"))
 	functionsServeCmd.Flags().BoolVar(noVerifyJWT, "no-verify-jwt", false, "Disable JWT verification for the Function.")
 	functionsServeCmd.Flags().StringVar(&envFilePath, "env-file", "", "Path to an env file to be populated to the Function environment.")
 	functionsServeCmd.Flags().StringVar(&importMapPath, "import-map", "", "Path to import map file.")
diff --git a/internal/functions/delete/delete.go b/internal/functions/delete/delete.go
index 3f9c966c..bd9c782a 100644
--- a/internal/functions/delete/delete.go
+++ b/internal/functions/delete/delete.go
@@ -19,26 +19,17 @@ func Run(ctx context.Context, slug string, projectRef string, fsys afero.Fs) err
 	}
 
 	// 2. Delete Function.
-	{
-		resp, err := utils.GetSupabase().GetFunctionWithResponse(ctx, projectRef, slug)
-		if err != nil {
-			return err
-		}
-
-		switch resp.StatusCode() {
-		case http.StatusNotFound: // Function doesn't exist
-			return errors.New("Function " + utils.Aqua(slug) + " does not exist on the Supabase project.")
-		case http.StatusOK: // Function exists
-			resp, err := utils.GetSupabase().DeleteFunctionWithResponse(ctx, projectRef, slug)
-			if err != nil {
-				return err
-			}
-			if resp.StatusCode() != http.StatusOK {
-				return errors.New("Failed to delete Function " + utils.Aqua(slug) + " on the Supabase project: " + string(resp.Body))
-			}
-		default:
-			return errors.New("Unexpected error deleting Function: " + string(resp.Body))
-		}
+	resp, err := utils.GetSupabase().DeleteFunctionWithResponse(ctx, projectRef, slug)
+	if err != nil {
+		return err
+	}
+	switch resp.StatusCode() {
+	case http.StatusNotFound:
+		return errors.New("Function " + utils.Aqua(slug) + " does not exist on the Supabase project.")
+	case http.StatusOK:
+		break
+	default:
+		return errors.New("Failed to delete Function " + utils.Aqua(slug) + " on the Supabase project: " + string(resp.Body))
 	}
 
 	fmt.Println("Deleted Function " + utils.Aqua(slug) + " from project " + utils.Aqua(projectRef) + ".")
diff --git a/internal/functions/deploy/deploy.go b/internal/functions/deploy/deploy.go
index c6b192a4..3f10b744 100644
--- a/internal/functions/deploy/deploy.go
+++ b/internal/functions/deploy/deploy.go
@@ -7,10 +7,8 @@ import (
 	"fmt"
 	"io"
 	"net/http"
-	"os"
 	"os/exec"
 	"path/filepath"
-	"strings"
 
 	"github.com/docker/go-units"
 	"github.com/spf13/afero"
@@ -20,9 +18,8 @@ import (
 
 const eszipContentType = "application/vnd.denoland.eszip"
 
-func Run(ctx context.Context, slug string, projectRef string, noVerifyJWT *bool, useLegacyBundle bool, importMapPath string, fsys afero.Fs) error {
+func Run(ctx context.Context, slug string, projectRef string, noVerifyJWT *bool, importMapPath string, fsys afero.Fs) error {
 	// 1. Sanity checks.
-	var scriptDir *utils.DenoScriptDir
 	{
 		// Load function config if any for fallbacks for some flags, but continue on error.
 		_ = utils.LoadConfigFS(fsys)
@@ -34,160 +31,111 @@ func Run(ctx context.Context, slug string, projectRef string, noVerifyJWT *bool,
 			}
 			noVerifyJWT = &x
 		}
-		if importMapPath != "" {
-			// skip
-		} else if functionConfig, ok := utils.Config.Functions[slug]; ok && functionConfig.ImportMap != "" {
-			if filepath.IsAbs(functionConfig.ImportMap) {
-				importMapPath = functionConfig.ImportMap
-			} else {
-				importMapPath = filepath.Join(utils.SupabaseDirPath, functionConfig.ImportMap)
-			}
-		} else if f, err := fsys.Stat(utils.FallbackImportMapPath); err == nil && !f.IsDir() {
-			importMapPath = utils.FallbackImportMapPath
+		resolved, err := utils.AbsImportMapPath(importMapPath, slug, fsys)
+		if err != nil {
+			return err
 		}
-		if importMapPath != "" {
-			if _, err := fsys.Stat(importMapPath); err != nil {
-				return fmt.Errorf("Failed to read import map: %w", err)
+		// Upstream server expects import map to be always defined
+		if importMapPath == "" {
+			resolved, err = filepath.Abs(utils.FallbackImportMapPath)
+			if err != nil {
+				return err
 			}
 		}
+		importMapPath = resolved
 		if err := utils.ValidateFunctionSlug(slug); err != nil {
 			return err
 		}
 		if err := utils.InstallOrUpgradeDeno(ctx, fsys); err != nil {
 			return err
 		}
-
-		var err error
-		scriptDir, err = utils.CopyDenoScripts(ctx, fsys)
-		if err != nil {
-			return err
-		}
 	}
 
 	// 2. Bundle Function.
-	var functionBody io.Reader
-	var functionSize int
-	{
-		fmt.Println("Bundling " + utils.Bold(slug))
-		denoPath, err := utils.GetDenoPath()
-		if err != nil {
-			return err
-		}
-
-		functionPath := filepath.Join(utils.FunctionsDir, slug)
-		if _, err := fsys.Stat(functionPath); errors.Is(err, os.ErrNotExist) {
-			// allow deploy from within supabase/
-			functionPath = filepath.Join("functions", slug)
-			if _, err := fsys.Stat(functionPath); errors.Is(err, os.ErrNotExist) {
-				// allow deploy from current directory
-				functionPath = slug
-			}
-		}
-
-		buildScriptPath := scriptDir.BuildPath
-		args := []string{"run", "-A", buildScriptPath, filepath.Join(functionPath, "index.ts"), importMapPath}
-		if useLegacyBundle {
-			args = []string{"bundle", "--no-check=remote", "--quiet", filepath.Join(functionPath, "index.ts")}
-		}
-		cmd := exec.CommandContext(ctx, denoPath, args...)
-		var outBuf, errBuf bytes.Buffer
-		cmd.Stdout = &outBuf
-		cmd.Stderr = &errBuf
-		if err := cmd.Run(); err != nil {
-			return fmt.Errorf("Error bundling function: %w\n%v", err, errBuf.String())
-		}
-
-		functionBody = &outBuf
-		functionSize = outBuf.Len()
+	scriptDir, err := utils.CopyDenoScripts(ctx, fsys)
+	if err != nil {
+		return err
+	}
+	entrypointPath, err := filepath.Abs(filepath.Join(utils.FunctionsDir, slug, "index.ts"))
+	if err != nil {
+		return err
+	}
+	fmt.Println("Bundling " + utils.Bold(slug))
+	functionBody, err := bundleFunction(ctx, entrypointPath, importMapPath, scriptDir.BuildPath, fsys)
+	if err != nil {
+		return err
 	}
 
 	// 3. Deploy new Function.
-	fmt.Println("Deploying " + utils.Bold(slug) + " (script size: " + utils.Bold(units.HumanSize(float64(functionSize))) + ")")
-	return deployFunction(ctx, projectRef, slug, functionBody, !*noVerifyJWT, useLegacyBundle)
+	functionSize := units.HumanSize(float64(functionBody.Len()))
+	fmt.Println("Deploying " + utils.Bold(slug) + " (script size: " + utils.Bold(functionSize) + ")")
+	return deployFunction(
+		ctx,
+		projectRef,
+		slug,
+		"file://"+entrypointPath,
+		"file://"+importMapPath,
+		!*noVerifyJWT,
+		functionBody,
+	)
 }
 
-func makeLegacyFunctionBody(functionBody io.Reader) (string, error) {
-	buf := new(strings.Builder)
-	_, err := io.Copy(buf, functionBody)
+func bundleFunction(ctx context.Context, entrypointPath, importMapPath, buildScriptPath string, fsys afero.Fs) (*bytes.Buffer, error) {
+	denoPath, err := utils.GetDenoPath()
 	if err != nil {
-		return "", err
+		return nil, err
 	}
-
-	return buf.String(), nil
+	// Bundle function and import_map with deno
+	args := []string{"run", "-A", buildScriptPath, entrypointPath, importMapPath}
+	cmd := exec.CommandContext(ctx, denoPath, args...)
+	var outBuf, errBuf bytes.Buffer
+	cmd.Stdout = &outBuf
+	cmd.Stderr = &errBuf
+	if err := cmd.Run(); err != nil {
+		return nil, fmt.Errorf("Error bundling function: %w\n%v", err, errBuf.String())
+	}
+	return &outBuf, nil
 }
 
-func deployFunction(ctx context.Context, projectRef, slug string, functionBody io.Reader, verifyJWT, useLegacyBundle bool) error {
-	{
-		resp, err := utils.GetSupabase().GetFunctionWithResponse(ctx, projectRef, slug)
+func deployFunction(ctx context.Context, projectRef, slug, entrypointUrl, importMapUrl string, verifyJWT bool, functionBody io.Reader) error {
+	resp, err := utils.GetSupabase().GetFunctionWithResponse(ctx, projectRef, slug)
+	if err != nil {
+		return err
+	}
+
+	switch resp.StatusCode() {
+	case http.StatusNotFound: // Function doesn't exist yet, so do a POST
+		resp, err := utils.GetSupabase().CreateFunctionWithBodyWithResponse(ctx, projectRef, &api.CreateFunctionParams{
+			Slug:           &slug,
+			Name:           &slug,
+			VerifyJwt:      &verifyJWT,
+			ImportMapPath:  &importMapUrl,
+			EntrypointPath: &entrypointUrl,
+		}, eszipContentType, functionBody)
 		if err != nil {
 			return err
 		}
-
-		var functionBodyStr string
-		if useLegacyBundle {
-			functionBodyStr, err = makeLegacyFunctionBody(functionBody)
-			if err != nil {
-				return err
-			}
+		if resp.JSON201 == nil {
+			return errors.New("Failed to create a new Function on the Supabase project: " + string(resp.Body))
 		}
-
-		// Note: imageMap is always set to true, since eszip created will always contain a `import_map.json`.
-		importMap := true
-
-		switch resp.StatusCode() {
-		case http.StatusNotFound: // Function doesn't exist yet, so do a POST
-			var resp *api.CreateFunctionResponse
-			var err error
-			if useLegacyBundle {
-				resp, err = utils.GetSupabase().CreateFunctionWithResponse(ctx, projectRef, &api.CreateFunctionParams{}, api.CreateFunctionJSONRequestBody{
-					Body:      functionBodyStr,
-					Name:      slug,
-					Slug:      slug,
-					VerifyJwt: &verifyJWT,
-				})
-			} else {
-				resp, err = utils.GetSupabase().CreateFunctionWithBodyWithResponse(ctx, projectRef, &api.CreateFunctionParams{
-					Slug:      &slug,
-					Name:      &slug,
-					VerifyJwt: &verifyJWT,
-					ImportMap: &importMap,
-				}, eszipContentType, functionBody)
-			}
-			if err != nil {
-				return err
-			}
-			if resp.JSON201 == nil {
-				return errors.New("Failed to create a new Function on the Supabase project: " + string(resp.Body))
-			}
-		case http.StatusOK: // Function already exists, so do a PATCH
-			var resp *api.UpdateFunctionResponse
-			var err error
-			if useLegacyBundle {
-				resp, err = utils.GetSupabase().UpdateFunctionWithResponse(ctx, projectRef, slug, &api.UpdateFunctionParams{}, api.UpdateFunctionJSONRequestBody{
-					Body:      &functionBodyStr,
-					VerifyJwt: &verifyJWT,
-				})
-			} else {
-				resp, err = utils.GetSupabase().UpdateFunctionWithBodyWithResponse(ctx, projectRef, slug, &api.UpdateFunctionParams{
-					VerifyJwt: &verifyJWT,
-					ImportMap: &importMap,
-				}, eszipContentType, functionBody)
-			}
-			if err != nil {
-				return err
-			}
-			if resp.JSON200 == nil {
-				return errors.New("Failed to update an existing Function's body on the Supabase project: " + string(resp.Body))
-			}
-		default:
-			return errors.New("Unexpected error deploying Function: " + string(resp.Body))
+	case http.StatusOK: // Function already exists, so do a PATCH
+		resp, err := utils.GetSupabase().UpdateFunctionWithBodyWithResponse(ctx, projectRef, slug, &api.UpdateFunctionParams{
+			VerifyJwt:      &verifyJWT,
+			ImportMapPath:  &importMapUrl,
+			EntrypointPath: &entrypointUrl,
+		}, eszipContentType, functionBody)
+		if err != nil {
+			return err
 		}
+		if resp.JSON200 == nil {
+			return errors.New("Failed to update an existing Function's body on the Supabase project: " + string(resp.Body))
+		}
+	default:
+		return errors.New("Unexpected error deploying Function: " + string(resp.Body))
 	}
 
 	fmt.Println("Deployed Function " + utils.Aqua(slug) + " on project " + utils.Aqua(projectRef))
-
 	url := fmt.Sprintf("%s/project/%v/functions/%v/details", utils.GetSupabaseDashboardURL(), projectRef, slug)
 	fmt.Println("You can inspect your deployment in the Dashboard: " + url)
-
 	return nil
 }
diff --git a/internal/functions/download/download.go b/internal/functions/download/download.go
index 7a7ab9f6..283a3bcf 100644
--- a/internal/functions/download/download.go
+++ b/internal/functions/download/download.go
@@ -12,6 +12,12 @@ import (
 
 	"github.com/spf13/afero"
 	"github.com/supabase/cli/internal/utils"
+	"github.com/supabase/cli/pkg/api"
+)
+
+var (
+	legacyEntrypointPath = "file:///src/index.ts"
+	legacyImportMapPath  = "file:///src/import_map.json"
 )
 
 func Run(ctx context.Context, slug string, projectRef string, fsys afero.Fs) error {
@@ -31,40 +37,68 @@ func Run(ctx context.Context, slug string, projectRef string, fsys afero.Fs) err
 	}
 
 	// 2. Download Function.
-	{
-		fmt.Println("Downloading " + utils.Bold(slug))
-		denoPath, err := utils.GetDenoPath()
-		if err != nil {
-			return err
-		}
+	if err := downloadFunction(ctx, projectRef, slug, scriptDir.ExtractPath); err != nil {
+		return err
+	}
 
-		resp, err := utils.GetSupabase().GetFunctionBodyWithResponse(ctx, projectRef, slug)
-		if err != nil {
-			return err
-		}
+	fmt.Println("Downloaded Function " + utils.Aqua(slug) + " from project " + utils.Aqua(projectRef) + ".")
+	return nil
+}
 
-		switch resp.StatusCode() {
-		case http.StatusNotFound: // Function doesn't exist
-			return errors.New("Function " + utils.Aqua(slug) + " does not exist on the Supabase project.")
-		case http.StatusOK: // Function exists
-			resBuf := bytes.NewReader(resp.Body)
+func getFunctionMetadata(ctx context.Context, projectRef, slug string) (*api.FunctionSlugResponse, error) {
+	resp, err := utils.GetSupabase().GetFunctionWithResponse(ctx, projectRef, slug)
+	if err != nil {
+		return nil, err
+	}
 
-			extractScriptPath := scriptDir.ExtractPath
-			funcDir := filepath.Join(utils.FunctionsDir, slug)
-			var errBuf bytes.Buffer
-			args := []string{"run", "-A", extractScriptPath, funcDir}
-			cmd := exec.CommandContext(ctx, denoPath, args...)
-			cmd.Stdin = resBuf
-			cmd.Stdout = os.Stdout
-			cmd.Stderr = &errBuf
-			if err := cmd.Run(); err != nil {
-				return fmt.Errorf("Error downloading function: %w\n%v", err, errBuf.String())
-			}
-		default:
-			return errors.New("Unexpected error downloading Function: " + string(resp.Body))
-		}
+	switch resp.StatusCode() {
+	case http.StatusNotFound:
+		return nil, errors.New("Function " + utils.Aqua(slug) + " does not exist on the Supabase project.")
+	case http.StatusOK:
+		break
+	default:
+		return nil, errors.New("Failed to download Function " + utils.Aqua(slug) + " on the Supabase project: " + string(resp.Body))
 	}
 
-	fmt.Println("Downloaded Function " + utils.Aqua(slug) + " from project " + utils.Aqua(projectRef) + ".")
+	if resp.JSON200.EntrypointPath == nil {
+		resp.JSON200.EntrypointPath = &legacyEntrypointPath
+	}
+	if resp.JSON200.ImportMapPath == nil {
+		resp.JSON200.ImportMapPath = &legacyImportMapPath
+	}
+	return resp.JSON200, nil
+}
+
+func downloadFunction(ctx context.Context, projectRef, slug, extractScriptPath string) error {
+	fmt.Println("Downloading " + utils.Bold(slug))
+	denoPath, err := utils.GetDenoPath()
+	if err != nil {
+		return err
+	}
+
+	meta, err := getFunctionMetadata(ctx, projectRef, slug)
+	if err != nil {
+		return err
+	}
+
+	resp, err := utils.GetSupabase().GetFunctionBodyWithResponse(ctx, projectRef, slug)
+	if err != nil {
+		return err
+	}
+	if resp.StatusCode() != http.StatusOK {
+		return errors.New("Unexpected error downloading Function: " + string(resp.Body))
+	}
+
+	resBuf := bytes.NewReader(resp.Body)
+	funcDir := filepath.Join(utils.FunctionsDir, slug)
+	args := []string{"run", "-A", extractScriptPath, funcDir, *meta.EntrypointPath}
+	cmd := exec.CommandContext(ctx, denoPath, args...)
+	var errBuf bytes.Buffer
+	cmd.Stdin = resBuf
+	cmd.Stdout = os.Stdout
+	cmd.Stderr = &errBuf
+	if err := cmd.Run(); err != nil {
+		return fmt.Errorf("Error downloading function: %w\n%v", err, errBuf.String())
+	}
 	return nil
 }
diff --git a/internal/functions/serve/serve.go b/internal/functions/serve/serve.go
index 8076f81c..b60021f7 100644
--- a/internal/functions/serve/serve.go
+++ b/internal/functions/serve/serve.go
@@ -56,32 +56,11 @@ func Run(ctx context.Context, slug string, envFilePath string, noVerifyJWT *bool
 				return fmt.Errorf("Failed to read env file: %w", err)
 			}
 		}
-		cwd, err := os.Getwd()
+		resolved, err := utils.AbsImportMapPath(importMapPath, slug, fsys)
 		if err != nil {
 			return err
 		}
-		if importMapPath != "" {
-			if !filepath.IsAbs(importMapPath) {
-				importMapPath = filepath.Join(cwd, importMapPath)
-			}
-		} else if functionConfig, ok := utils.Config.Functions[slug]; ok && functionConfig.ImportMap != "" {
-			if filepath.IsAbs(functionConfig.ImportMap) {
-				importMapPath = functionConfig.ImportMap
-			} else {
-				importMapPath = filepath.Join(cwd, utils.SupabaseDirPath, functionConfig.ImportMap)
-			}
-		} else if f, err := fsys.Stat(utils.FallbackImportMapPath); err == nil && !f.IsDir() {
-			if filepath.IsAbs(utils.FallbackImportMapPath) {
-				importMapPath = utils.FallbackImportMapPath
-			} else {
-				importMapPath = filepath.Join(cwd, utils.FallbackImportMapPath)
-			}
-		}
-		if importMapPath != "" {
-			if _, err := fsys.Stat(importMapPath); err != nil {
-				return fmt.Errorf("Failed to read import map: %w", err)
-			}
-		}
+		importMapPath = resolved
 	}
 
 	// 2. Parse user defined env
diff --git a/internal/utils/deno.go b/internal/utils/deno.go
index 105748fe..1bd3582f 100644
--- a/internal/utils/deno.go
+++ b/internal/utils/deno.go
@@ -320,3 +320,28 @@ func GetPathHash(path string) string {
 	digest := sha256.Sum256([]byte(path))
 	return hex.EncodeToString(digest[:])
 }
+
+func AbsImportMapPath(importMapPath, slug string, fsys afero.Fs) (string, error) {
+	if importMapPath == "" {
+		if functionConfig, ok := Config.Functions[slug]; ok && functionConfig.ImportMap != "" {
+			importMapPath = functionConfig.ImportMap
+			if !filepath.IsAbs(importMapPath) {
+				importMapPath = filepath.Join(SupabaseDirPath, importMapPath)
+			}
+		} else if exists, _ := afero.Exists(fsys, FallbackImportMapPath); exists {
+			importMapPath = FallbackImportMapPath
+		} else {
+			return importMapPath, nil
+		}
+	}
+	resolved, err := filepath.Abs(importMapPath)
+	if err != nil {
+		return "", err
+	}
+	if f, err := fsys.Stat(resolved); err != nil {
+		return "", fmt.Errorf("Failed to read import map: %w", err)
+	} else if f.IsDir() {
+		return "", errors.New("Importing directory is unsupported: " + resolved)
+	}
+	return resolved, nil
+}
diff --git a/internal/utils/denos/build.ts b/internal/utils/denos/build.ts
index 322e7508..8b73203e 100644
--- a/internal/utils/denos/build.ts
+++ b/internal/utils/denos/build.ts
@@ -4,37 +4,16 @@ import { writeAll } from "https://deno.land/std@0.162.0/streams/conversion.ts";
 import { compress } from "https://deno.land/x/brotli@0.1.7/mod.ts";
 import { build } from "https://deno.land/x/eszip@v0.35.0/mod.ts";
 
-const virtualBasePath = "file:///src/";
+async function buildAndWrite(entrypointPath: string, importMapPath: string) {
+  const entrypointUrl = path.toFileUrl(entrypointPath).href
+  const importMapUrl = path.toFileUrl(importMapPath).href
 
-async function buildAndWrite(p: string, importMapPath: string) {
-  const funcDirPath = path.dirname(p);
-  try {
-    await Deno.lstat(funcDirPath);
-  } catch (e) {
-    console.error(
-      `Error: Cannot access "${funcDirPath}". Check if directory exists and has read permissions.`,
-    );
-    Deno.exit(1);
-  }
-
-  const entrypoint = new URL("index.ts", virtualBasePath).href;
-
-  const eszip = await build([entrypoint], async (specifier: string) => {
+  const eszip = await build([entrypointUrl], async (specifier: string) => {
     const url = new URL(specifier);
     if (url.protocol === "file:") {
       console.error(specifier);
-      // if the path is `file:///*`, treat it as a path from parent directory
-      let actualPath = specifier.replace("file:///", `./${funcDirPath}/../`);
-      // if the path is `file:///src/*`, treat it as a relative path from current dir
-      if (specifier.startsWith(virtualBasePath)) {
-        actualPath = specifier.replace(virtualBasePath, `./${funcDirPath}/`);
-      }
+      const actualPath = url.pathname;
 
-      // If an import map path is set read file from the given path.
-      // Otherwise default to `import_map.json` in functions directory.
-      if (specifier.endsWith("import_map.json") && importMapPath) {
-        actualPath = importMapPath;
-      }
       try {
         const content = await Deno.readTextFile(actualPath);
         return {
@@ -45,7 +24,7 @@ async function buildAndWrite(p: string, importMapPath: string) {
       } catch (e) {
         if (
           (e instanceof Deno.errors.NotFound) &&
-          actualPath.endsWith("import_map.json")
+          actualPath === importMapPath
         ) {
           // if there's no import_map.json, set an empty one
           return {
@@ -60,7 +39,7 @@ async function buildAndWrite(p: string, importMapPath: string) {
     }
 
     return load(specifier);
-  }, "file:///src/import_map.json");
+  }, importMapUrl);
   // compress ESZIP payload using Brotli
   const compressed = compress(eszip);
 
@@ -74,7 +53,7 @@ async function buildAndWrite(p: string, importMapPath: string) {
   await writeAll(Deno.stdout, combinedPayload);
 }
 
-buildAndWrite(Deno.args[0], Deno.args[1]);
+buildAndWrite(...Deno.args);
 
 // Adapted from https://github.com/denoland/deno/blob/bacbf949256e32ca84e7f11c0171db7d9a644b44/cli/auth_tokens.rs#L38
 
diff --git a/internal/utils/denos/extract.ts b/internal/utils/denos/extract.ts
index fe030972..becaa63f 100644
--- a/internal/utils/denos/extract.ts
+++ b/internal/utils/denos/extract.ts
@@ -1,14 +1,8 @@
 import * as path from "https://deno.land/std@0.127.0/path/mod.ts";
 import { readAll } from "https://deno.land/std@0.162.0/streams/conversion.ts";
 import { decompress } from "https://deno.land/x/brotli@0.1.7/mod.ts";
-
 import { Parser } from "https://deno.land/x/eszip@v0.30.0/mod.ts";
 
-function url2path(url: string) {
-  const srcPath = new URL(url).pathname.replace("/src", "");
-  return path.join(...srcPath.split("/").filter(Boolean));
-}
-
 async function write(p: string, content: string) {
   await Deno.mkdir(path.dirname(p), { recursive: true });
   await Deno.writeTextFile(p, content);
@@ -21,25 +15,35 @@ async function loadEszip(bytes: Uint8Array) {
   return { parser, specifiers };
 }
 
-async function extractEszip(dest: string, parser, specifiers) {
-  const imports = {};
-
+async function extractEszip(
+  destPath: string,
+  entrypointUrl: string,
+  parser: Parser,
+  specifiers: string[]
+) {
+  const entrypointPath = path.fromFileUrl(entrypointUrl);
+  const basePath = path.dirname(entrypointPath);
   for (const specifier of specifiers) {
     // skip remote dependencies
     if (!specifier.startsWith("file:")) {
       continue;
     }
+    console.error(specifier);
     const module = await parser.getModuleSource(specifier);
-    await write(path.join(dest, url2path(specifier)), module);
+    const absPath = path.fromFileUrl(specifier);
+    const relPath = path.relative(basePath, absPath);
+    const dest = path.join(destPath, relPath);
+    console.info(path.resolve(dest));
+    await write(dest, module);
   }
 }
 
-async function extractSource(dest: string) {
+async function extractSource(destPath: string, entrypointUrl: string) {
   const buf = await readAll(Deno.stdin);
   // response is compressed with Brotli
   const decompressed = decompress(buf);
   const { parser, specifiers } = await loadEszip(decompressed);
-  await extractEszip(dest, parser, specifiers);
+  await extractEszip(destPath, entrypointUrl, parser, specifiers);
 }
 
-extractSource(Deno.args[0]);
+extractSource(...Deno.args);
diff --git a/package.json b/package.json
index d8fa5765..ea22bbc4 100644
--- a/package.json
+++ b/package.json
@@ -20,7 +20,7 @@
   "dependencies": {
     "bin-links": "^4.0.1",
     "node-fetch": "^3.2.10",
-    "tar": "6.1.14"
+    "tar": "6.1.15"
   },
   "release": {
     "branches": [
diff --git a/pkg/api/client.gen.go b/pkg/api/client.gen.go
index 6281f4a6..75d6772c 100644
--- a/pkg/api/client.gen.go
+++ b/pkg/api/client.gen.go
@@ -2417,6 +2417,38 @@ func NewCreateFunctionRequestWithBody(server string, ref string, params *CreateF
 
 	}
 
+	if params.EntrypointPath != nil {
+
+		if queryFrag, err := runtime.StyleParamWithLocation("form", true, "entrypoint_path", runtime.ParamLocationQuery, *params.EntrypointPath); err != nil {
+			return nil, err
+		} else if parsed, err := url.ParseQuery(queryFrag); err != nil {
+			return nil, err
+		} else {
+			for k, v := range parsed {
+				for _, v2 := range v {
+					queryValues.Add(k, v2)
+				}
+			}
+		}
+
+	}
+
+	if params.ImportMapPath != nil {
+
+		if queryFrag, err := runtime.StyleParamWithLocation("form", true, "import_map_path", runtime.ParamLocationQuery, *params.ImportMapPath); err != nil {
+			return nil, err
+		} else if parsed, err := url.ParseQuery(queryFrag); err != nil {
+			return nil, err
+		} else {
+			for k, v := range parsed {
+				for _, v2 := range v {
+					queryValues.Add(k, v2)
+				}
+			}
+		}
+
+	}
+
 	queryURL.RawQuery = queryValues.Encode()
 
 	req, err := http.NewRequest("POST", queryURL.String(), body)
@@ -2621,6 +2653,38 @@ func NewUpdateFunctionRequestWithBody(server string, ref string, functionSlug st
 
 	}
 
+	if params.EntrypointPath != nil {
+
+		if queryFrag, err := runtime.StyleParamWithLocation("form", true, "entrypoint_path", runtime.ParamLocationQuery, *params.EntrypointPath); err != nil {
+			return nil, err
+		} else if parsed, err := url.ParseQuery(queryFrag); err != nil {
+			return nil, err
+		} else {
+			for k, v := range parsed {
+				for _, v2 := range v {
+					queryValues.Add(k, v2)
+				}
+			}
+		}
+
+	}
+
+	if params.ImportMapPath != nil {
+
+		if queryFrag, err := runtime.StyleParamWithLocation("form", true, "import_map_path", runtime.ParamLocationQuery, *params.ImportMapPath); err != nil {
+			return nil, err
+		} else if parsed, err := url.ParseQuery(queryFrag); err != nil {
+			return nil, err
+		} else {
+			for k, v := range parsed {
+				for _, v2 := range v {
+					queryValues.Add(k, v2)
+				}
+			}
+		}
+
+	}
+
 	queryURL.RawQuery = queryValues.Encode()
 
 	req, err := http.NewRequest("PATCH", queryURL.String(), body)
diff --git a/pkg/api/types.gen.go b/pkg/api/types.gen.go
index 6193f512..8ab6105a 100644
--- a/pkg/api/types.gen.go
+++ b/pkg/api/types.gen.go
@@ -353,15 +353,17 @@ type Domain struct {
 
 // FunctionResponse defines model for FunctionResponse.
 type FunctionResponse struct {
-	CreatedAt float32                `json:"created_at"`
-	Id        string                 `json:"id"`
-	ImportMap *bool                  `json:"import_map,omitempty"`
-	Name      string                 `json:"name"`
-	Slug      string                 `json:"slug"`
-	Status    FunctionResponseStatus `json:"status"`
-	UpdatedAt float32                `json:"updated_at"`
-	VerifyJwt *bool                  `json:"verify_jwt,omitempty"`
-	Version   float32                `json:"version"`
+	CreatedAt      float32                `json:"created_at"`
+	EntrypointPath *string                `json:"entrypoint_path,omitempty"`
+	Id             string                 `json:"id"`
+	ImportMap      *bool                  `json:"import_map,omitempty"`
+	ImportMapPath  *string                `json:"import_map_path,omitempty"`
+	Name           string                 `json:"name"`
+	Slug           string                 `json:"slug"`
+	Status         FunctionResponseStatus `json:"status"`
+	UpdatedAt      float32                `json:"updated_at"`
+	VerifyJwt      *bool                  `json:"verify_jwt,omitempty"`
+	Version        float32                `json:"version"`
 }
 
 // FunctionResponseStatus defines model for FunctionResponse.Status.
@@ -369,15 +371,17 @@ type FunctionResponseStatus string
 
 // FunctionSlugResponse defines model for FunctionSlugResponse.
 type FunctionSlugResponse struct {
-	CreatedAt float32                    `json:"created_at"`
-	Id        string                     `json:"id"`
-	ImportMap *bool                      `json:"import_map,omitempty"`
-	Name      string                     `json:"name"`
-	Slug      string                     `json:"slug"`
-	Status    FunctionSlugResponseStatus `json:"status"`
-	UpdatedAt float32                    `json:"updated_at"`
-	VerifyJwt *bool                      `json:"verify_jwt,omitempty"`
-	Version   float32                    `json:"version"`
+	CreatedAt      float32                    `json:"created_at"`
+	EntrypointPath *string                    `json:"entrypoint_path,omitempty"`
+	Id             string                     `json:"id"`
+	ImportMap      *bool                      `json:"import_map,omitempty"`
+	ImportMapPath  *string                    `json:"import_map_path,omitempty"`
+	Name           string                     `json:"name"`
+	Slug           string                     `json:"slug"`
+	Status         FunctionSlugResponseStatus `json:"status"`
+	UpdatedAt      float32                    `json:"updated_at"`
+	VerifyJwt      *bool                      `json:"verify_jwt,omitempty"`
+	Version        float32                    `json:"version"`
 }
 
 // FunctionSlugResponseStatus defines model for FunctionSlugResponse.Status.
@@ -735,18 +739,22 @@ type TokenParamsGrantType string
 
 // CreateFunctionParams defines parameters for CreateFunction.
 type CreateFunctionParams struct {
-	Slug      *string `form:"slug,omitempty" json:"slug,omitempty"`
-	Name      *string `form:"name,omitempty" json:"name,omitempty"`
-	VerifyJwt *bool   `form:"verify_jwt,omitempty" json:"verify_jwt,omitempty"`
-	ImportMap *bool   `form:"import_map,omitempty" json:"import_map,omitempty"`
+	Slug           *string `form:"slug,omitempty" json:"slug,omitempty"`
+	Name           *string `form:"name,omitempty" json:"name,omitempty"`
+	VerifyJwt      *bool   `form:"verify_jwt,omitempty" json:"verify_jwt,omitempty"`
+	ImportMap      *bool   `form:"import_map,omitempty" json:"import_map,omitempty"`
+	EntrypointPath *string `form:"entrypoint_path,omitempty" json:"entrypoint_path,omitempty"`
+	ImportMapPath  *string `form:"import_map_path,omitempty" json:"import_map_path,omitempty"`
 }
 
 // UpdateFunctionParams defines parameters for UpdateFunction.
 type UpdateFunctionParams struct {
-	Slug      *string `form:"slug,omitempty" json:"slug,omitempty"`
-	Name      *string `form:"name,omitempty" json:"name,omitempty"`
-	VerifyJwt *bool   `form:"verify_jwt,omitempty" json:"verify_jwt,omitempty"`
-	ImportMap *bool   `form:"import_map,omitempty" json:"import_map,omitempty"`
+	Slug           *string `form:"slug,omitempty" json:"slug,omitempty"`
+	Name           *string `form:"name,omitempty" json:"name,omitempty"`
+	VerifyJwt      *bool   `form:"verify_jwt,omitempty" json:"verify_jwt,omitempty"`
+	ImportMap      *bool   `form:"import_map,omitempty" json:"import_map,omitempty"`
+	EntrypointPath *string `form:"entrypoint_path,omitempty" json:"entrypoint_path,omitempty"`
+	ImportMapPath  *string `form:"import_map_path,omitempty" json:"import_map_path,omitempty"`
 }
 
 // DeleteSecretsJSONBody defines parameters for DeleteSecrets.


Test patch:
diff --git a/internal/functions/delete/delete_test.go b/internal/functions/delete/delete_test.go
index 0585ac19..7a58e76c 100644
--- a/internal/functions/delete/delete_test.go
+++ b/internal/functions/delete/delete_test.go
@@ -10,30 +10,27 @@ import (
 	"github.com/stretchr/testify/assert"
 	"github.com/supabase/cli/internal/testing/apitest"
 	"github.com/supabase/cli/internal/utils"
-	"github.com/supabase/cli/pkg/api"
 	"gopkg.in/h2non/gock.v1"
 )
 
 func TestDeleteCommand(t *testing.T) {
+	const slug = "test-func"
+	// Setup valid project ref
+	project := apitest.RandomProjectRef()
+	// Setup valid access token
+	token := apitest.RandomAccessToken(t)
+	t.Setenv("SUPABASE_ACCESS_TOKEN", string(token))
+
 	t.Run("deletes function from project", func(t *testing.T) {
 		// Setup in-memory fs
 		fsys := afero.NewMemMapFs()
-		// Setup valid project ref
-		project := apitest.RandomProjectRef()
-		// Setup valid access token
-		token := apitest.RandomAccessToken(t)
-		t.Setenv("SUPABASE_ACCESS_TOKEN", string(token))
 		// Setup mock api
 		defer gock.OffAll()
 		gock.New(utils.DefaultApiHost).
-			Get("/v1/projects/" + project + "/functions/test-func").
-			Reply(http.StatusOK).
-			JSON(api.FunctionResponse{Id: "1"})
-		gock.New(utils.DefaultApiHost).
-			Delete("/v1/projects/" + project + "/functions/test-func").
+			Delete("/v1/projects/" + project + "/functions/" + slug).
 			Reply(http.StatusOK)
 		// Run test
-		assert.NoError(t, Run(context.Background(), "test-func", project, fsys))
+		assert.NoError(t, Run(context.Background(), slug, project, fsys))
 		// Validate api
 		assert.Empty(t, apitest.ListUnmatchedRequests())
 	})
@@ -52,108 +49,46 @@ func TestDeleteCommand(t *testing.T) {
 	t.Run("throws error on network failure", func(t *testing.T) {
 		// Setup in-memory fs
 		fsys := afero.NewMemMapFs()
-		// Setup valid project ref
-		project := apitest.RandomProjectRef()
-		// Setup valid access token
-		token := apitest.RandomAccessToken(t)
-		t.Setenv("SUPABASE_ACCESS_TOKEN", string(token))
 		// Setup mock api
 		defer gock.OffAll()
 		gock.New(utils.DefaultApiHost).
-			Get("/v1/projects/" + project + "/functions/test-func").
+			Delete("/v1/projects/" + project + "/functions/" + slug).
 			ReplyError(errors.New("network error"))
 		// Run test
-		err := Run(context.Background(), "test-func", project, fsys)
+		err := Run(context.Background(), slug, project, fsys)
 		// Check error
 		assert.ErrorContains(t, err, "network error")
 		assert.Empty(t, apitest.ListUnmatchedRequests())
 	})
 
-	t.Run("throws error on service unavailable", func(t *testing.T) {
-		// Setup in-memory fs
-		fsys := afero.NewMemMapFs()
-		// Setup valid project ref
-		project := apitest.RandomProjectRef()
-		// Setup valid access token
-		token := apitest.RandomAccessToken(t)
-		t.Setenv("SUPABASE_ACCESS_TOKEN", string(token))
-		// Setup mock api
-		defer gock.OffAll()
-		gock.New(utils.DefaultApiHost).
-			Get("/v1/projects/" + project + "/functions/test-func").
-			Reply(http.StatusServiceUnavailable)
-		// Run test
-		err := Run(context.Background(), "test-func", project, fsys)
-		// Check error
-		assert.ErrorContains(t, err, "Unexpected error deleting Function:")
-		assert.Empty(t, apitest.ListUnmatchedRequests())
-	})
-
 	t.Run("throws error on missing function", func(t *testing.T) {
 		// Setup in-memory fs
 		fsys := afero.NewMemMapFs()
-		// Setup valid project ref
-		project := apitest.RandomProjectRef()
-		// Setup valid access token
-		token := apitest.RandomAccessToken(t)
-		t.Setenv("SUPABASE_ACCESS_TOKEN", string(token))
 		// Setup mock api
 		defer gock.OffAll()
 		gock.New(utils.DefaultApiHost).
-			Get("/v1/projects/" + project + "/functions/test-func").
-			Reply(http.StatusNotFound)
+			Delete("/v1/projects/" + project + "/functions/" + slug).
+			Reply(http.StatusNotFound).
+			JSON(map[string]string{"message": "Function not found"})
 		// Run test
-		err := Run(context.Background(), "test-func", project, fsys)
+		err := Run(context.Background(), slug, project, fsys)
 		// Check error
 		assert.ErrorContains(t, err, "Function test-func does not exist on the Supabase project.")
 		assert.Empty(t, apitest.ListUnmatchedRequests())
 	})
 
-	t.Run("throws error on delete failure", func(t *testing.T) {
+	t.Run("throws error on service unavailable", func(t *testing.T) {
 		// Setup in-memory fs
 		fsys := afero.NewMemMapFs()
-		// Setup valid project ref
-		project := apitest.RandomProjectRef()
-		// Setup valid access token
-		token := apitest.RandomAccessToken(t)
-		t.Setenv("SUPABASE_ACCESS_TOKEN", string(token))
 		// Setup mock api
 		defer gock.OffAll()
 		gock.New(utils.DefaultApiHost).
-			Get("/v1/projects/" + project + "/functions/test-func").
-			Reply(http.StatusOK).
-			JSON(api.FunctionResponse{Id: "1"})
-		gock.New(utils.DefaultApiHost).
-			Delete("/v1/projects/" + project + "/functions/test-func").
+			Delete("/v1/projects/" + project + "/functions/" + slug).
 			Reply(http.StatusServiceUnavailable)
 		// Run test
-		err := Run(context.Background(), "test-func", project, fsys)
+		err := Run(context.Background(), slug, project, fsys)
 		// Check error
 		assert.ErrorContains(t, err, "Failed to delete Function test-func on the Supabase project:")
 		assert.Empty(t, apitest.ListUnmatchedRequests())
 	})
-
-	t.Run("throws error on delete network failure", func(t *testing.T) {
-		// Setup in-memory fs
-		fsys := afero.NewMemMapFs()
-		// Setup valid project ref
-		project := apitest.RandomProjectRef()
-		// Setup valid access token
-		token := apitest.RandomAccessToken(t)
-		t.Setenv("SUPABASE_ACCESS_TOKEN", string(token))
-		// Setup mock api
-		defer gock.OffAll()
-		gock.New(utils.DefaultApiHost).
-			Get("/v1/projects/" + project + "/functions/test-func").
-			Reply(http.StatusOK).
-			JSON(api.FunctionResponse{Id: "1"})
-		gock.New(utils.DefaultApiHost).
-			Delete("/v1/projects/" + project + "/functions/test-func").
-			ReplyError(errors.New("network error"))
-		// Run test
-		err := Run(context.Background(), "test-func", project, fsys)
-		// Check error
-		assert.ErrorContains(t, err, "network error")
-		assert.Empty(t, apitest.ListUnmatchedRequests())
-	})
 }
diff --git a/internal/functions/deploy/deploy_test.go b/internal/functions/deploy/deploy_test.go
index 88fefb42..4a5baf29 100644
--- a/internal/functions/deploy/deploy_test.go
+++ b/internal/functions/deploy/deploy_test.go
@@ -4,13 +4,12 @@ import (
 	"archive/zip"
 	"bytes"
 	"context"
-	"encoding/json"
 	"errors"
 	"fmt"
-	"io"
 	"log"
 	"net/http"
 	"os"
+	"path/filepath"
 	"strings"
 	"testing"
 
@@ -43,36 +42,9 @@ func TestMain(m *testing.M) {
 }
 
 func TestDeployCommand(t *testing.T) {
-	t.Run("deploys new function (legacy bundle)", func(t *testing.T) {
-		const slug = "test-func"
-		// Setup in-memory fs
-		fsys := afero.NewMemMapFs()
-		// Setup valid project ref
-		project := apitest.RandomProjectRef()
-		// 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/" + project + "/functions/" + slug).
-			Reply(http.StatusNotFound)
-		gock.New(utils.DefaultApiHost).
-			Post("/v1/projects/" + project + "/functions").
-			Reply(http.StatusCreated).
-			JSON(api.FunctionResponse{Id: "1"})
-		// Run test
-		noVerifyJWT := true
-		assert.NoError(t, Run(context.Background(), slug, project, &noVerifyJWT, true, "", fsys))
-		// Validate api
-		assert.Empty(t, apitest.ListUnmatchedRequests())
-	})
+	const slug = "test-func"
 
 	t.Run("deploys new function (ESZIP)", func(t *testing.T) {
-		const slug = "test-func"
 		// Setup in-memory fs
 		fsys := afero.NewMemMapFs()
 		// Setup valid project ref
@@ -94,41 +66,12 @@ func TestDeployCommand(t *testing.T) {
 			JSON(api.FunctionResponse{Id: "1"})
 		// Run test
 		noVerifyJWT := true
-		assert.NoError(t, Run(context.Background(), slug, project, &noVerifyJWT, false, "", fsys))
-		// Validate api
-		assert.Empty(t, apitest.ListUnmatchedRequests())
-	})
-
-	t.Run("updates deployed function (legacy bundle)", func(t *testing.T) {
-		const slug = "test-func"
-		// Setup in-memory fs
-		fsys := afero.NewMemMapFs()
-		// Setup valid project ref
-		project := apitest.RandomProjectRef()
-		// 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/" + project + "/functions/" + slug).
-			Reply(http.StatusOK).
-			JSON(api.FunctionResponse{Id: "1"})
-		gock.New(utils.DefaultApiHost).
-			Patch("/v1/projects/" + project + "/functions/" + slug).
-			Reply(http.StatusOK).
-			JSON(api.FunctionResponse{Id: "1"})
-		// Run test
-		assert.NoError(t, Run(context.Background(), slug, project, nil, true, "", fsys))
+		assert.NoError(t, Run(context.Background(), slug, project, &noVerifyJWT, "", fsys))
 		// Validate api
 		assert.Empty(t, apitest.ListUnmatchedRequests())
 	})
 
 	t.Run("updates deployed function (ESZIP)", func(t *testing.T) {
-		const slug = "test-func"
 		// Setup in-memory fs
 		fsys := afero.NewMemMapFs()
 		// Setup valid project ref
@@ -150,7 +93,7 @@ func TestDeployCommand(t *testing.T) {
 			Reply(http.StatusOK).
 			JSON(api.FunctionResponse{Id: "1"})
 		// Run test
-		assert.NoError(t, Run(context.Background(), slug, project, nil, false, "", fsys))
+		assert.NoError(t, Run(context.Background(), slug, project, nil, "", fsys))
 		// Validate api
 		assert.Empty(t, apitest.ListUnmatchedRequests())
 	})
@@ -162,7 +105,7 @@ func TestDeployCommand(t *testing.T) {
 		project := apitest.RandomProjectRef()
 		// Run test
 		noVerifyJWT := true
-		err := Run(context.Background(), "@", project, &noVerifyJWT, true, "", fsys)
+		err := Run(context.Background(), "@", project, &noVerifyJWT, "", fsys)
 		// Check error
 		assert.ErrorContains(t, err, "Invalid Function name.")
 	})
@@ -174,7 +117,7 @@ func TestDeployCommand(t *testing.T) {
 		project := apitest.RandomProjectRef()
 		// Run test
 		noVerifyJWT := true
-		err := Run(context.Background(), "test-func", project, &noVerifyJWT, true, "", fsys)
+		err := Run(context.Background(), "test-func", project, &noVerifyJWT, "", fsys)
 		// Check error
 		assert.ErrorContains(t, err, "operation not permitted")
 	})
@@ -201,7 +144,7 @@ func TestDeployCommand(t *testing.T) {
 			Body(&body)
 		// Run test
 		noVerifyJWT := true
-		err = Run(context.Background(), "test-func", project, &noVerifyJWT, true, "", fsys)
+		err = Run(context.Background(), "test-func", project, &noVerifyJWT, "", fsys)
 		// Check error
 		assert.ErrorContains(t, err, "Error bundling function: exit status 1\nbundle failed\n")
 		assert.Empty(t, apitest.ListUnmatchedRequests())
@@ -229,13 +172,12 @@ func TestDeployCommand(t *testing.T) {
 			Body(&body)
 
 		noVerifyJWT := true
-		err = Run(context.Background(), "test-func", project, &noVerifyJWT, false, "", fsys)
+		err = Run(context.Background(), "test-func", project, &noVerifyJWT, "", fsys)
 		// Check error
 		assert.ErrorContains(t, err, "Error bundling function: exit status 1\neszip failed\n")
 	})
 
 	t.Run("verify_jwt param falls back to config", func(t *testing.T) {
-		const slug = "test-func"
 		// Setup in-memory fs
 		fsys := afero.NewMemMapFs()
 		require.NoError(t, utils.WriteConfig(fsys, false))
@@ -261,31 +203,17 @@ verify_jwt = false
 			Get("/v1/projects/" + project + "/functions/" + slug).
 			Reply(http.StatusNotFound)
 		gock.New(utils.DefaultApiHost).
-			Post("/v1/projects/" + project + "/functions").
-			AddMatcher(func(req *http.Request, ereq *gock.Request) (bool, error) {
-				body, err := io.ReadAll(req.Body)
-				if err != nil {
-					return false, err
-				}
-
-				var bodyJson map[string]interface{}
-				err = json.Unmarshal(body, &bodyJson)
-				if err != nil {
-					return false, err
-				}
-
-				return bodyJson["verify_jwt"] == false, nil
-			}).
+			Post("/v1/projects/"+project+"/functions").
+			MatchParam("verify_jwt", "false").
 			Reply(http.StatusCreated).
 			JSON(api.FunctionResponse{Id: "1"})
 		// Run test
-		assert.NoError(t, Run(context.Background(), slug, project, nil, true, "", fsys))
+		assert.NoError(t, Run(context.Background(), slug, project, nil, "", fsys))
 		// Validate api
 		assert.Empty(t, apitest.ListUnmatchedRequests())
 	})
 
 	t.Run("verify_jwt flag overrides config", func(t *testing.T) {
-		const slug = "test-func"
 		// Setup in-memory fs
 		fsys := afero.NewMemMapFs()
 		require.NoError(t, utils.WriteConfig(fsys, false))
@@ -311,43 +239,32 @@ verify_jwt = false
 			Get("/v1/projects/" + project + "/functions/" + slug).
 			Reply(http.StatusNotFound)
 		gock.New(utils.DefaultApiHost).
-			Post("/v1/projects/" + project + "/functions").
-			AddMatcher(func(req *http.Request, ereq *gock.Request) (bool, error) {
-				body, err := io.ReadAll(req.Body)
-				if err != nil {
-					return false, err
-				}
-
-				var bodyJson map[string]interface{}
-				err = json.Unmarshal(body, &bodyJson)
-				if err != nil {
-					return false, err
-				}
-
-				return bodyJson["verify_jwt"] == true, nil
-			}).
+			Post("/v1/projects/"+project+"/functions").
+			MatchParam("verify_jwt", "true").
 			Reply(http.StatusCreated).
 			JSON(api.FunctionResponse{Id: "1"})
 		// Run test
 		noVerifyJwt := false
-		assert.NoError(t, Run(context.Background(), slug, project, &noVerifyJwt, true, "", fsys))
+		assert.NoError(t, Run(context.Background(), slug, project, &noVerifyJwt, "", fsys))
 		// Validate api
 		assert.Empty(t, apitest.ListUnmatchedRequests())
 	})
 
 	t.Run("uses fallback import map", func(t *testing.T) {
-		const slug = "test-func"
 		// Setup in-memory fs
 		fsys := afero.NewMemMapFs()
 		require.NoError(t, utils.WriteConfig(fsys, false))
-		require.NoError(t, afero.WriteFile(fsys, "supabase/functions/import_map.json", []byte(""), 0644))
+		require.NoError(t, afero.WriteFile(fsys, utils.FallbackImportMapPath, []byte(""), 0644))
+		absPath, err := filepath.Abs(utils.FallbackImportMapPath)
+		require.NoError(t, err)
+		require.NoError(t, afero.WriteFile(fsys, absPath, []byte("{}"), 0644))
 		// Setup valid project ref
 		project := apitest.RandomProjectRef()
 		// Setup valid access token
 		token := apitest.RandomAccessToken(t)
 		t.Setenv("SUPABASE_ACCESS_TOKEN", string(token))
 		// Setup valid deno path
-		_, err := fsys.Create(utils.DenoPathOverride)
+		_, err = fsys.Create(utils.DenoPathOverride)
 		require.NoError(t, err)
 		// Setup mock api
 		defer gock.OffAll()
@@ -356,12 +273,12 @@ verify_jwt = false
 			Reply(http.StatusNotFound)
 		gock.New(utils.DefaultApiHost).
 			Post("/v1/projects/"+project+"/functions").
-			MatchParam("import_map", "true").
+			MatchParam("import_map_path", absPath).
 			Reply(http.StatusCreated).
 			JSON(api.FunctionResponse{Id: "1"})
 		// Run test
 		noVerifyJwt := false
-		assert.NoError(t, Run(context.Background(), slug, project, &noVerifyJwt, false, "", fsys))
+		assert.NoError(t, Run(context.Background(), slug, project, &noVerifyJwt, "", fsys))
 		// Validate api
 		assert.Empty(t, apitest.ListUnmatchedRequests())
 	})
@@ -382,7 +299,7 @@ func TestDeployFunction(t *testing.T) {
 			Get("/v1/projects/" + project + "/functions/" + slug).
 			ReplyError(errors.New("network error"))
 		// Run test
-		err := deployFunction(context.Background(), project, slug, strings.NewReader("body"), true, true)
+		err := deployFunction(context.Background(), project, slug, "", "", true, strings.NewReader("body"))
 		// Check error
 		assert.ErrorContains(t, err, "network error")
 	})
@@ -394,7 +311,7 @@ func TestDeployFunction(t *testing.T) {
 			Get("/v1/projects/" + project + "/functions/" + slug).
 			Reply(http.StatusServiceUnavailable)
 		// Run test
-		err := deployFunction(context.Background(), project, slug, strings.NewReader("body"), true, true)
+		err := deployFunction(context.Background(), project, slug, "", "", true, strings.NewReader("body"))
 		// Check error
 		assert.ErrorContains(t, err, "Unexpected error deploying Function:")
 	})
@@ -409,7 +326,7 @@ func TestDeployFunction(t *testing.T) {
 			Post("/v1/projects/" + project + "/functions").
 			ReplyError(errors.New("network error"))
 		// Run test
-		err := deployFunction(context.Background(), project, slug, strings.NewReader("body"), true, true)
+		err := deployFunction(context.Background(), project, slug, "", "", true, strings.NewReader("body"))
 		// Check error
 		assert.ErrorContains(t, err, "network error")
 	})
@@ -424,7 +341,7 @@ func TestDeployFunction(t *testing.T) {
 			Post("/v1/projects/" + project + "/functions").
 			Reply(http.StatusServiceUnavailable)
 		// Run test
-		err := deployFunction(context.Background(), project, slug, strings.NewReader("body"), true, true)
+		err := deployFunction(context.Background(), project, slug, "", "", true, strings.NewReader("body"))
 		// Check error
 		assert.ErrorContains(t, err, "Failed to create a new Function on the Supabase project:")
 	})
@@ -440,7 +357,7 @@ func TestDeployFunction(t *testing.T) {
 			Patch("/v1/projects/" + project + "/functions/" + slug).
 			ReplyError(errors.New("network error"))
 		// Run test
-		err := deployFunction(context.Background(), project, slug, strings.NewReader("body"), true, true)
+		err := deployFunction(context.Background(), project, slug, "", "", true, strings.NewReader("body"))
 		// Check error
 		assert.ErrorContains(t, err, "network error")
 	})
@@ -456,7 +373,7 @@ func TestDeployFunction(t *testing.T) {
 			Patch("/v1/projects/" + project + "/functions/" + slug).
 			Reply(http.StatusServiceUnavailable)
 		// Run test
-		err := deployFunction(context.Background(), project, slug, strings.NewReader("body"), true, true)
+		err := deployFunction(context.Background(), project, slug, "", "", true, strings.NewReader("body"))
 		// Check error
 		assert.ErrorContains(t, err, "Failed to update an existing Function's body on the Supabase project:")
 	})
diff --git a/internal/functions/download/download_test.go b/internal/functions/download/download_test.go
new file mode 100644
index 00000000..6e586158
--- /dev/null
+++ b/internal/functions/download/download_test.go
@@ -0,0 +1,246 @@
+package download
+
+import (
+	"archive/zip"
+	"bytes"
+	"context"
+	"errors"
+	"fmt"
+	"log"
+	"net/http"
+	"os"
+	"testing"
+
+	"github.com/spf13/afero"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	"github.com/supabase/cli/internal/testing/apitest"
+	"github.com/supabase/cli/internal/utils"
+	"github.com/supabase/cli/pkg/api"
+	"gopkg.in/h2non/gock.v1"
+)
+
+func TestMain(m *testing.M) {
+	// Setup fake deno binary
+	if len(os.Args) > 1 && (os.Args[1] == "bundle" || os.Args[1] == "upgrade" || os.Args[1] == "run") {
+		msg := os.Getenv("TEST_DENO_ERROR")
+		if msg != "" {
+			fmt.Fprintln(os.Stderr, msg)
+			os.Exit(1)
+		}
+		os.Exit(0)
+	}
+	denoPath, err := os.Executable()
+	if err != nil {
+		log.Fatalln(err)
+	}
+	utils.DenoPathOverride = denoPath
+	// Run test suite
+	os.Exit(m.Run())
+}
+
+func TestDownloadCommand(t *testing.T) {
+	const slug = "test-func"
+
+	t.Run("downloads eszip bundle", func(t *testing.T) {
+		// Setup in-memory fs
+		fsys := afero.NewMemMapFs()
+		// Setup valid project ref
+		project := apitest.RandomProjectRef()
+		// 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/" + project + "/functions/" + slug).
+			Reply(http.StatusOK).
+			JSON(api.FunctionResponse{Id: "1"})
+		gock.New(utils.DefaultApiHost).
+			Get("/v1/projects/" + project + "/functions/" + slug + "/body").
+			Reply(http.StatusOK)
+		// Run test
+		err = Run(context.Background(), slug, project, fsys)
+		// Check error
+		assert.NoError(t, err)
+		assert.Empty(t, apitest.ListUnmatchedRequests())
+	})
+
+	t.Run("throws error on malformed slug", func(t *testing.T) {
+		// Setup in-memory fs
+		fsys := afero.NewMemMapFs()
+		// Setup valid project ref
+		project := apitest.RandomProjectRef()
+		// Run test
+		err := Run(context.Background(), "@", project, fsys)
+		// Check error
+		assert.ErrorContains(t, err, "Invalid Function name.")
+	})
+
+	t.Run("throws error on failure to install deno", func(t *testing.T) {
+		// Setup in-memory fs
+		fsys := afero.NewReadOnlyFs(afero.NewMemMapFs())
+		// Setup valid project ref
+		project := apitest.RandomProjectRef()
+		// Run test
+		err := Run(context.Background(), slug, project, fsys)
+		// Check error
+		assert.ErrorContains(t, err, "operation not permitted")
+	})
+
+	t.Run("throws error on copy failure", func(t *testing.T) {
+		// Setup in-memory fs
+		fsys := afero.NewMemMapFs()
+		// Setup valid project ref
+		project := apitest.RandomProjectRef()
+		// Setup valid deno path
+		_, err := fsys.Create(utils.DenoPathOverride)
+		require.NoError(t, err)
+		// Run test
+		err = Run(context.Background(), slug, project, afero.NewReadOnlyFs(fsys))
+		// Check error
+		assert.ErrorContains(t, err, "operation not permitted")
+	})
+
+	t.Run("throws error on missing function", func(t *testing.T) {
+		// Setup in-memory fs
+		fsys := afero.NewMemMapFs()
+		// Setup valid project ref
+		project := apitest.RandomProjectRef()
+		// 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/" + project + "/functions/" + slug).
+			Reply(http.StatusNotFound).
+			JSON(map[string]string{"message": "Function not found"})
+		// Run test
+		err = Run(context.Background(), slug, project, fsys)
+		// Check error
+		assert.ErrorContains(t, err, "Function test-func does not exist on the Supabase project.")
+	})
+}
+
+func TestDownloadFunction(t *testing.T) {
+	const slug = "test-func"
+	// Setup valid project ref
+	project := apitest.RandomProjectRef()
+	// Setup valid access token
+	token := apitest.RandomAccessToken(t)
+	t.Setenv("SUPABASE_ACCESS_TOKEN", string(token))
+
+	t.Run("throws error on network error", func(t *testing.T) {
+		// Setup mock api
+		defer gock.OffAll()
+		gock.New(utils.DefaultApiHost).
+			Get("/v1/projects/" + project + "/functions/" + slug).
+			Reply(http.StatusOK).
+			JSON(api.FunctionResponse{Id: "1"})
+		gock.New(utils.DefaultApiHost).
+			Get("/v1/projects/" + project + "/functions/" + slug + "/body").
+			ReplyError(errors.New("network error"))
+		// Run test
+		err := downloadFunction(context.Background(), project, slug, "")
+		// Check error
+		assert.ErrorContains(t, err, "network error")
+	})
+
+	t.Run("throws error on service unavailable", func(t *testing.T) {
+		// Setup mock api
+		defer gock.OffAll()
+		gock.New(utils.DefaultApiHost).
+			Get("/v1/projects/" + project + "/functions/" + slug).
+			Reply(http.StatusOK).
+			JSON(api.FunctionResponse{Id: "1"})
+		gock.New(utils.DefaultApiHost).
+			Get("/v1/projects/" + project + "/functions/" + slug + "/body").
+			Reply(http.StatusServiceUnavailable)
+		// Run test
+		err := downloadFunction(context.Background(), project, slug, "")
+		// Check error
+		assert.ErrorContains(t, err, "Unexpected error downloading Function:")
+	})
+
+	t.Run("throws error on extract failure", func(t *testing.T) {
+		// Setup deno error
+		t.Setenv("TEST_DENO_ERROR", "extract failed")
+		var body bytes.Buffer
+		archive := zip.NewWriter(&body)
+		w, err := archive.Create("deno")
+		require.NoError(t, err)
+		_, err = w.Write([]byte("binary"))
+		require.NoError(t, err)
+		require.NoError(t, archive.Close())
+		// Setup mock api
+		defer gock.OffAll()
+		gock.New(utils.DefaultApiHost).
+			Get("/v1/projects/" + project + "/functions/" + slug).
+			Reply(http.StatusOK).
+			JSON(api.FunctionResponse{Id: "1"})
+		gock.New(utils.DefaultApiHost).
+			Get("/v1/projects/" + project + "/functions/" + slug + "/body").
+			Reply(http.StatusOK)
+		// Run test
+		err = downloadFunction(context.Background(), project, slug, "")
+		// Check error
+		assert.ErrorContains(t, err, "Error downloading function: exit status 1\nextract failed\n")
+		assert.Empty(t, apitest.ListUnmatchedRequests())
+	})
+}
+
+func TestGetMetadata(t *testing.T) {
+	const slug = "test-func"
+	project := apitest.RandomProjectRef()
+	// Setup valid access token
+	token := apitest.RandomAccessToken(t)
+	t.Setenv("SUPABASE_ACCESS_TOKEN", string(token))
+
+	t.Run("fallback to default paths", func(t *testing.T) {
+		// Setup mock api
+		defer gock.OffAll()
+		gock.New(utils.DefaultApiHost).
+			Get("/v1/projects/" + project + "/functions/" + slug).
+			Reply(http.StatusOK).
+			JSON(api.FunctionResponse{Id: "1"})
+		// Run test
+		meta, err := getFunctionMetadata(context.Background(), project, slug)
+		// Check error
+		assert.NoError(t, err)
+		assert.Equal(t, legacyEntrypointPath, *meta.EntrypointPath)
+		assert.Equal(t, legacyImportMapPath, *meta.ImportMapPath)
+	})
+
+	t.Run("throws error on network error", func(t *testing.T) {
+		// Setup mock api
+		defer gock.OffAll()
+		gock.New(utils.DefaultApiHost).
+			Get("/v1/projects/" + project + "/functions/" + slug).
+			ReplyError(errors.New("network error"))
+		// Run test
+		meta, err := getFunctionMetadata(context.Background(), project, slug)
+		// Check error
+		assert.ErrorContains(t, err, "network error")
+		assert.Nil(t, meta)
+	})
+
+	t.Run("throws error on service unavailable", func(t *testing.T) {
+		// Setup mock api
+		defer gock.OffAll()
+		gock.New(utils.DefaultApiHost).
+			Get("/v1/projects/" + project + "/functions/" + slug).
+			Reply(http.StatusServiceUnavailable)
+		// Run test
+		meta, err := getFunctionMetadata(context.Background(), project, slug)
+		// Check error
+		assert.ErrorContains(t, err, "Failed to download Function test-func on the Supabase project:")
+		assert.Nil(t, meta)
+	})
+}
diff --git a/internal/utils/deno_test.go b/internal/utils/deno_test.go
index c6332be4..9a5840bf 100644
--- a/internal/utils/deno_test.go
+++ b/internal/utils/deno_test.go
@@ -54,3 +54,70 @@ func TestResolveImports(t *testing.T) {
 		assert.Equal(t, "https://deno.land", resolved.Scopes["my-scope"]["my-mod"])
 	})
 }
+
+func TestImportMapPath(t *testing.T) {
+	t.Run("loads import map from default location", func(t *testing.T) {
+		// Setup in-memory fs
+		fsys := afero.NewMemMapFs()
+		require.NoError(t, afero.WriteFile(fsys, FallbackImportMapPath, []byte("{}"), 0644))
+		absPath, err := filepath.Abs(FallbackImportMapPath)
+		require.NoError(t, err)
+		require.NoError(t, afero.WriteFile(fsys, absPath, []byte("{}"), 0644))
+		// Run test
+		resolved, err := AbsImportMapPath("", "", fsys)
+		// Check error
+		assert.NoError(t, err)
+		assert.Equal(t, absPath, resolved)
+	})
+
+	t.Run("per function config takes precedence", func(t *testing.T) {
+		slug := "hello"
+		Config.Functions = map[string]function{
+			slug: {ImportMap: "import_map.json"},
+		}
+		absPath, err := filepath.Abs("supabase/import_map.json")
+		require.NoError(t, err)
+		// Setup in-memory fs
+		fsys := afero.NewMemMapFs()
+		require.NoError(t, afero.WriteFile(fsys, FallbackImportMapPath, []byte("{}"), 0644))
+		require.NoError(t, afero.WriteFile(fsys, absPath, []byte("{}"), 0644))
+		// Run test
+		resolved, err := AbsImportMapPath("", slug, fsys)
+		// Check error
+		assert.NoError(t, err)
+		assert.Equal(t, absPath, resolved)
+	})
+
+	t.Run("returns empty string if no fallback", func(t *testing.T) {
+		// Setup in-memory fs
+		fsys := afero.NewMemMapFs()
+		// Run test
+		resolved, err := AbsImportMapPath("", "", fsys)
+		// Check error
+		assert.NoError(t, err)
+		assert.Empty(t, resolved)
+	})
+
+	t.Run("throws error on missing file", func(t *testing.T) {
+		path := "/tmp/import_map"
+		// Setup in-memory fs
+		fsys := afero.NewMemMapFs()
+		// Run test
+		resolved, err := AbsImportMapPath(path, "", fsys)
+		// Check error
+		assert.ErrorIs(t, err, os.ErrNotExist)
+		assert.Empty(t, resolved)
+	})
+
+	t.Run("throws error on importing directory", func(t *testing.T) {
+		path := "/tmp/import_map"
+		// Setup in-memory fs
+		fsys := afero.NewMemMapFs()
+		require.NoError(t, fsys.MkdirAll(path, 0755))
+		// Run test
+		resolved, err := AbsImportMapPath(path, "", fsys)
+		// Check error
+		assert.ErrorContains(t, err, "Importing directory is unsupported: "+path)
+		assert.Empty(t, resolved)
+	})
+}