Repository: supabase/cli
Pull request #678 (2022-12-08 04:57:34)

feat: allow providing custom import-map paths to function serve/deploy
## What kind of change does this PR introduce?

This PR allows users to set a custom `--import-map` flag with relative path to an import map. 

This covers following use cases:
* Creating a project-level import_map.json outside of `supabase/functions` to be shared across all functions in the project.
* If you run `deno vendor`, those vendored dependencies needs to be linked using `vendor/import_map.json`



Patch:
diff --git a/cmd/functions.go b/cmd/functions.go
index e17d6965..59a9f473 100644
--- a/cmd/functions.go
+++ b/cmd/functions.go
@@ -44,6 +44,7 @@ var (
 
 	noVerifyJWT     bool
 	useLegacyBundle bool
+	importMapPath   string
 
 	functionsDeployCmd = &cobra.Command{
 		Use:   "deploy <Function name>",
@@ -59,7 +60,7 @@ var (
 				return err
 			}
 			ctx, _ := signal.NotifyContext(cmd.Context(), os.Interrupt)
-			return deploy.Run(ctx, args[0], projectRef, !noVerifyJWT, useLegacyBundle, fsys)
+			return deploy.Run(ctx, args[0], projectRef, !noVerifyJWT, useLegacyBundle, importMapPath, fsys)
 		},
 	}
 
@@ -81,7 +82,7 @@ var (
 		Args:  cobra.ExactArgs(1),
 		RunE: func(cmd *cobra.Command, args []string) error {
 			ctx, _ := signal.NotifyContext(cmd.Context(), os.Interrupt)
-			return serve.Run(ctx, args[0], envFilePath, !noVerifyJWT, afero.NewOsFs())
+			return serve.Run(ctx, args[0], envFilePath, !noVerifyJWT, importMapPath, afero.NewOsFs())
 		},
 	}
 )
@@ -91,8 +92,10 @@ func init() {
 	functionsDeployCmd.Flags().BoolVar(&noVerifyJWT, "no-verify-jwt", false, "Disable JWT verification for the Function.")
 	functionsDeployCmd.Flags().StringVar(&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.")
 	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.")
 	functionsCmd.AddCommand(functionsDeleteCmd)
 	functionsCmd.AddCommand(functionsDeployCmd)
 	functionsCmd.AddCommand(functionsNewCmd)
diff --git a/internal/functions/deploy/deploy.go b/internal/functions/deploy/deploy.go
index e5e94df4..1667144f 100644
--- a/internal/functions/deploy/deploy.go
+++ b/internal/functions/deploy/deploy.go
@@ -20,7 +20,7 @@ import (
 
 const EszipContentType = "application/vnd.denoland.eszip"
 
-func Run(ctx context.Context, slug string, projectRefArg string, verifyJWT bool, useLegacyBundle bool, fsys afero.Fs) error {
+func Run(ctx context.Context, slug string, projectRefArg string, verifyJWT bool, useLegacyBundle bool, importMapPath string, fsys afero.Fs) error {
 	// 1. Sanity checks.
 	projectRef := projectRefArg
 	var buildScriptPath string
@@ -68,7 +68,7 @@ func Run(ctx context.Context, slug string, projectRefArg string, verifyJWT bool,
 			}
 		}
 
-		args := []string{"run", "-A", buildScriptPath, filepath.Join(functionPath, "index.ts")}
+		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")}
 		}
diff --git a/internal/functions/serve/serve.go b/internal/functions/serve/serve.go
index 8e9cf32e..acfb4f32 100644
--- a/internal/functions/serve/serve.go
+++ b/internal/functions/serve/serve.go
@@ -18,10 +18,11 @@ import (
 )
 
 const (
-	relayFuncDir = "/home/deno/functions"
+	relayFuncDir              = "/home/deno/functions"
+	customDockerImportMapPath = "/home/deno/import_map.json"
 )
 
-func Run(ctx context.Context, slug string, envFilePath string, verifyJWT bool, fsys afero.Fs) error {
+func Run(ctx context.Context, slug string, envFilePath string, verifyJWT bool, importMapPath string, fsys afero.Fs) error {
 	// 1. Sanity checks.
 	{
 		if err := utils.AssertSupabaseCliIsSetUpFS(fsys); err != nil {
@@ -41,6 +42,11 @@ func Run(ctx context.Context, slug string, envFilePath string, verifyJWT bool, f
 				return fmt.Errorf("Failed to read env file: %w", err)
 			}
 		}
+		if importMapPath != "" {
+			if _, err := fsys.Stat(importMapPath); err != nil {
+				return fmt.Errorf("Failed to read import map: %w", err)
+			}
+		}
 	}
 
 	// 3. Start relay.
@@ -65,6 +71,11 @@ func Run(ctx context.Context, slug string, envFilePath string, verifyJWT bool, f
 			return err
 		}
 
+		binds := []string{filepath.Join(cwd, utils.FunctionsDir) + ":" + relayFuncDir + ":ro,z"}
+		// If a import map path is explcitly provided, mount it as a separate file
+		if importMapPath != "" {
+			binds = append(binds, filepath.Join(cwd, importMapPath)+":"+customDockerImportMapPath+":ro,z")
+		}
 		if _, err := utils.DockerStart(
 			ctx,
 			container.Config{
@@ -72,7 +83,7 @@ func Run(ctx context.Context, slug string, envFilePath string, verifyJWT bool, f
 				Env:   env,
 			},
 			container.HostConfig{
-				Binds: []string{filepath.Join(cwd, utils.FunctionsDir) + ":" + relayFuncDir + ":ro,z"},
+				Binds: binds,
 			},
 			utils.DenoRelayId,
 		); err != nil {
@@ -95,11 +106,17 @@ func Run(ctx context.Context, slug string, envFilePath string, verifyJWT bool, f
 	// 4. Start Function.
 	localFuncDir := filepath.Join(utils.FunctionsDir, slug)
 	localImportMapPath := filepath.Join(localFuncDir, "import_map.json")
+
 	// We assume the image is always Linux, so path separator must always be `/`.
 	// We can't use filepath.Join because it uses the path separator for the host system, which is `\` for Windows.
 	dockerFuncPath := relayFuncDir + "/" + slug + "/index.ts"
 	dockerImportMapPath := relayFuncDir + "/" + slug + "/import_map.json"
 
+	if importMapPath != "" {
+		localImportMapPath = importMapPath
+		dockerImportMapPath = customDockerImportMapPath
+	}
+
 	denoCacheCmd := []string{"deno", "cache"}
 	{
 		if _, err := fsys.Stat(localImportMapPath); err == nil {
diff --git a/internal/utils/eszip/build.ts b/internal/utils/eszip/build.ts
index 7c15cd19..2ded37e2 100644
--- a/internal/utils/eszip/build.ts
+++ b/internal/utils/eszip/build.ts
@@ -7,7 +7,7 @@ import { load } from "https://deno.land/x/eszip@v0.30.0/loader.ts";
 
 const virtualBasePath = "file:///src/";
 
-async function buildAndWrite(p: string) {
+async function buildAndWrite(p: string, importMapPath: string) {
   const funcDirPath = path.dirname(p);
   const entrypoint = new URL("index.ts", virtualBasePath).href;
 
@@ -21,6 +21,12 @@ async function buildAndWrite(p: string) {
       if (specifier.startsWith(virtualBasePath)) {
         actualPath = specifier.replace(virtualBasePath, `./${funcDirPath}/`);
       }
+
+      // 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 {
@@ -57,4 +63,4 @@ async function buildAndWrite(p: string) {
   await writeAll(Deno.stdout, combinedPayload);
 }
 
-buildAndWrite(Deno.args[0]);
+buildAndWrite(Deno.args[0], Deno.args[1]);


Test patch:
diff --git a/internal/functions/deploy/deploy_test.go b/internal/functions/deploy/deploy_test.go
index 2dd05d46..65af4f48 100644
--- a/internal/functions/deploy/deploy_test.go
+++ b/internal/functions/deploy/deploy_test.go
@@ -63,7 +63,7 @@ func TestDeployCommand(t *testing.T) {
 			Reply(http.StatusCreated).
 			JSON(api.FunctionResponse{Id: "1"})
 		// Run test
-		assert.NoError(t, Run(context.Background(), slug, project, false, true, fsys))
+		assert.NoError(t, Run(context.Background(), slug, project, false, true, "", fsys))
 		// Validate api
 		assert.Empty(t, apitest.ListUnmatchedRequests())
 	})
@@ -90,7 +90,7 @@ func TestDeployCommand(t *testing.T) {
 			Reply(http.StatusCreated).
 			JSON(api.FunctionResponse{Id: "1"})
 		// Run test
-		assert.NoError(t, Run(context.Background(), slug, project, false, false, fsys))
+		assert.NoError(t, Run(context.Background(), slug, project, false, false, "", fsys))
 		// Validate api
 		assert.Empty(t, apitest.ListUnmatchedRequests())
 	})
@@ -119,7 +119,7 @@ func TestDeployCommand(t *testing.T) {
 			Reply(http.StatusOK).
 			JSON(api.FunctionResponse{Id: "1"})
 		// Run test
-		assert.NoError(t, Run(context.Background(), slug, "", true, true, fsys))
+		assert.NoError(t, Run(context.Background(), slug, "", true, true, "", fsys))
 		// Validate api
 		assert.Empty(t, apitest.ListUnmatchedRequests())
 	})
@@ -148,7 +148,7 @@ func TestDeployCommand(t *testing.T) {
 			Reply(http.StatusOK).
 			JSON(api.FunctionResponse{Id: "1"})
 		// Run test
-		assert.NoError(t, Run(context.Background(), slug, "", true, false, fsys))
+		assert.NoError(t, Run(context.Background(), slug, "", true, false, "", fsys))
 		// Validate api
 		assert.Empty(t, apitest.ListUnmatchedRequests())
 	})
@@ -159,7 +159,7 @@ func TestDeployCommand(t *testing.T) {
 		// Setup invalid project ref
 		require.NoError(t, afero.WriteFile(fsys, utils.ProjectRefPath, []byte("test-project"), 0644))
 		// Run test
-		err := Run(context.Background(), "test-func", "", false, true, fsys)
+		err := Run(context.Background(), "test-func", "", false, true, "", fsys)
 		// Check error
 		assert.ErrorContains(t, err, "Invalid project ref format.")
 	})
@@ -168,7 +168,7 @@ func TestDeployCommand(t *testing.T) {
 		// Setup in-memory fs
 		fsys := afero.NewMemMapFs()
 		// Run test
-		err := Run(context.Background(), "test-func", "test-project", false, true, fsys)
+		err := Run(context.Background(), "test-func", "test-project", false, true, "", fsys)
 		// Check error
 		assert.ErrorContains(t, err, "Invalid project ref format.")
 	})
@@ -179,7 +179,7 @@ func TestDeployCommand(t *testing.T) {
 		// Setup valid project ref
 		project := apitest.RandomProjectRef()
 		// Run test
-		err := Run(context.Background(), "@", project, false, true, fsys)
+		err := Run(context.Background(), "@", project, false, true, "", fsys)
 		// Check error
 		assert.ErrorContains(t, err, "Invalid Function name.")
 	})
@@ -190,7 +190,7 @@ func TestDeployCommand(t *testing.T) {
 		// Setup valid project ref
 		project := apitest.RandomProjectRef()
 		// Run test
-		err := Run(context.Background(), "test-func", project, false, true, fsys)
+		err := Run(context.Background(), "test-func", project, false, true, "", fsys)
 		// Check error
 		assert.ErrorContains(t, err, "operation not permitted")
 	})
@@ -216,7 +216,7 @@ func TestDeployCommand(t *testing.T) {
 			Reply(http.StatusOK).
 			Body(&body)
 		// Run test
-		err = Run(context.Background(), "test-func", project, false, true, fsys)
+		err = Run(context.Background(), "test-func", project, false, true, "", fsys)
 		// Check error
 		assert.ErrorContains(t, err, "Error bundling function: exit status 1\nbundle failed\n")
 		assert.Empty(t, apitest.ListUnmatchedRequests())
@@ -243,7 +243,7 @@ func TestDeployCommand(t *testing.T) {
 			Reply(http.StatusOK).
 			Body(&body)
 
-		err = Run(context.Background(), "test-func", project, false, false, fsys)
+		err = Run(context.Background(), "test-func", project, false, false, "", fsys)
 		// Check error
 		assert.ErrorContains(t, err, "Error bundling function: exit status 1\neszip failed\n")
 	})
diff --git a/internal/functions/serve/serve_test.go b/internal/functions/serve/serve_test.go
index d933e19b..3737179e 100644
--- a/internal/functions/serve/serve_test.go
+++ b/internal/functions/serve/serve_test.go
@@ -35,7 +35,7 @@ func TestServeCommand(t *testing.T) {
 			Post("/v" + utils.Docker.ClientVersion() + "/containers").
 			Reply(http.StatusServiceUnavailable)
 		// Run test
-		err := Run(context.Background(), "test-func", "", true, fsys)
+		err := Run(context.Background(), "test-func", "", true, "", fsys)
 		// Check error
 		assert.ErrorContains(t, err, "request returned Service Unavailable for API route and version http://localhost/v1.41/containers/supabase_deno_relay_serve/exec")
 		assert.Empty(t, apitest.ListUnmatchedRequests())