Skip to content

Commit 3dfb4a1

Browse files
fix: outside import with relative module graph (#3417)
* refactor: removing duplicated `ImportMap` struct * fix: resolving relative outside imports It applies the import map walking to resolve the Module Graph * refactor: revert some changes from 7a43b81 Moving `ImportMap` struct to `functions/pkg` and importing it inside internals * stamp: lint * stamp: lint & using unix separator * stamp: copying file buffer directly to writer - we don't need to tee here, just copy from file to the writer will do. * stamp: removing duplicated code for `ImportMap.Resolve()` - using `afero.NewIOFS(fsys)` to cast to `io.FS` * stamp: use absolute filepath while bundling - Convert `srcPath` inside `addFile`, since docker mount always expect an absolute path * chore: bind relative imports in source files * chore: refactor bind host modules * chore: update unit tests * fix: change import map flag to relative path * chore: update unit tests * chore: test for relative entrypoint --------- Co-authored-by: Qiao Han <[email protected]>
1 parent 96a407f commit 3dfb4a1

File tree

10 files changed

+104
-145
lines changed

10 files changed

+104
-145
lines changed

internal/functions/deploy/bundle.go

Lines changed: 11 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -122,41 +122,24 @@ func GetBindMounts(cwd, hostFuncDir, hostOutputDir, hostEntrypointPath, hostImpo
122122
binds = append(binds, hostOutputDir+":"+dockerOutputDir+":rw")
123123
}
124124
}
125-
// Allow entrypoints outside the functions directory
126-
hostEntrypointDir := filepath.Dir(hostEntrypointPath)
127-
if len(hostEntrypointDir) > 0 {
128-
if !filepath.IsAbs(hostEntrypointDir) {
129-
hostEntrypointDir = filepath.Join(cwd, hostEntrypointDir)
130-
}
131-
if !strings.HasSuffix(hostEntrypointDir, sep) {
132-
hostEntrypointDir += sep
133-
}
134-
if !strings.HasPrefix(hostEntrypointDir, hostFuncDir) &&
135-
!strings.HasPrefix(hostEntrypointDir, hostOutputDir) {
136-
dockerEntrypointDir := utils.ToDockerPath(hostEntrypointDir)
137-
binds = append(binds, hostEntrypointDir+":"+dockerEntrypointDir+":ro")
138-
}
125+
// Imports outside of ./supabase/functions will be bound by walking the entrypoint
126+
modules, err := utils.BindHostModules(cwd, hostEntrypointPath, hostImportMapPath, fsys)
127+
if err != nil {
128+
return nil, err
139129
}
140-
// Imports outside of ./supabase/functions will be bound by absolute path
141130
if len(hostImportMapPath) > 0 {
142131
if !filepath.IsAbs(hostImportMapPath) {
143132
hostImportMapPath = filepath.Join(cwd, hostImportMapPath)
144133
}
145-
importMap, err := utils.NewImportMap(hostImportMapPath, fsys)
146-
if err != nil {
147-
return nil, err
148-
}
149-
modules := importMap.BindHostModules()
150134
dockerImportMapPath := utils.ToDockerPath(hostImportMapPath)
151135
modules = append(modules, hostImportMapPath+":"+dockerImportMapPath+":ro")
152-
// Remove any duplicate mount points
153-
for _, mod := range modules {
154-
hostPath := strings.Split(mod, ":")[0]
155-
if !strings.HasPrefix(hostPath, hostFuncDir) &&
156-
(len(hostOutputDir) == 0 || !strings.HasPrefix(hostPath, hostOutputDir)) &&
157-
(len(hostEntrypointDir) == 0 || !strings.HasPrefix(hostPath, hostEntrypointDir)) {
158-
binds = append(binds, mod)
159-
}
136+
}
137+
// Remove any duplicate mount points
138+
for _, mod := range modules {
139+
hostPath := strings.Split(mod, ":")[0]
140+
if !strings.HasPrefix(hostPath, hostFuncDir) &&
141+
(len(hostOutputDir) == 0 || !strings.HasPrefix(hostPath, hostOutputDir)) {
142+
binds = append(binds, mod)
160143
}
161144
}
162145
return binds, nil

internal/functions/deploy/bundle_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ func TestDockerBundle(t *testing.T) {
2929
t.Run("throws error on bundle failure", func(t *testing.T) {
3030
// Setup in-memory fs
3131
fsys := afero.NewMemMapFs()
32-
absImportMap := filepath.Join(cwd, "hello", "deno.json")
32+
absImportMap := filepath.Join("hello", "deno.json")
3333
require.NoError(t, utils.WriteFile(absImportMap, []byte("{}"), fsys))
3434
// Setup deno error
3535
t.Setenv("TEST_DENO_ERROR", "bundle failed")

internal/functions/deploy/deploy.go

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,19 @@ func Run(ctx context.Context, slugs []string, useDocker bool, noVerifyJWT *bool,
3232
if len(slugs) == 0 {
3333
return errors.Errorf("No Functions specified or found in %s", utils.Bold(utils.FunctionsDir))
3434
}
35+
// Flag import map is specified relative to current directory instead of workdir
36+
cwd, err := os.Getwd()
37+
if err != nil {
38+
return errors.Errorf("failed to get working directory: %w", err)
39+
}
40+
if len(importMapPath) > 0 {
41+
if !filepath.IsAbs(importMapPath) {
42+
importMapPath = filepath.Join(utils.CurrentDirAbs, importMapPath)
43+
}
44+
if importMapPath, err = filepath.Rel(cwd, importMapPath); err != nil {
45+
return errors.Errorf("failed to resolve relative path: %w", err)
46+
}
47+
}
3548
functionConfig, err := GetFunctionConfig(slugs, importMapPath, noVerifyJWT, fsys)
3649
if err != nil {
3750
return err
@@ -83,10 +96,6 @@ func GetFunctionConfig(slugs []string, importMapPath string, noVerifyJWT *bool,
8396
} else if err != nil {
8497
return nil, errors.Errorf("failed to fallback import map: %w", err)
8598
}
86-
// Flag import map is specified relative to current directory instead of workdir
87-
if len(importMapPath) > 0 && !filepath.IsAbs(importMapPath) {
88-
importMapPath = filepath.Join(utils.CurrentDirAbs, importMapPath)
89-
}
9099
functionConfig := make(config.FunctionConfig, len(slugs))
91100
for _, name := range slugs {
92101
function, ok := utils.Config.Functions[name]

internal/functions/deploy/deploy_test.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,7 @@ import_map = "./import_map.json"
8484
`)
8585
require.NoError(t, err)
8686
require.NoError(t, f.Close())
87-
importMapPath, err := filepath.Abs(filepath.Join(utils.SupabaseDirPath, "import_map.json"))
88-
require.NoError(t, err)
87+
importMapPath := filepath.Join(utils.SupabaseDirPath, "import_map.json")
8988
require.NoError(t, afero.WriteFile(fsys, importMapPath, []byte("{}"), 0644))
9089
// Setup function entrypoint
9190
entrypointPath := filepath.Join(utils.FunctionsDir, slug, "index.ts")

internal/functions/serve/serve.go

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,8 @@ const (
6464
dockerRuntimeInspectorPort = 8083
6565
)
6666

67-
var (
68-
//go:embed templates/main.ts
69-
mainFuncEmbed string
70-
)
67+
//go:embed templates/main.ts
68+
var mainFuncEmbed string
7169

7270
func Run(ctx context.Context, envFilePath string, noVerifyJWT *bool, importMapPath string, runtimeOption RuntimeOption, fsys afero.Fs) error {
7371
// 1. Sanity checks.
@@ -121,6 +119,14 @@ func ServeFunctions(ctx context.Context, envFilePath string, noVerifyJWT *bool,
121119
if err != nil {
122120
return errors.Errorf("failed to get working directory: %w", err)
123121
}
122+
if len(importMapPath) > 0 {
123+
if !filepath.IsAbs(importMapPath) {
124+
importMapPath = filepath.Join(utils.CurrentDirAbs, importMapPath)
125+
}
126+
if importMapPath, err = filepath.Rel(cwd, importMapPath); err != nil {
127+
return errors.Errorf("failed to resolve relative path: %w", err)
128+
}
129+
}
124130
binds, functionsConfigString, err := populatePerFunctionConfigs(cwd, importMapPath, noVerifyJWT, fsys)
125131
if err != nil {
126132
return err

internal/functions/serve/serve_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ func TestServeCommand(t *testing.T) {
8888
})
8989

9090
t.Run("throws error on missing import map", func(t *testing.T) {
91+
utils.CurrentDirAbs = "/"
9192
// Setup in-memory fs
9293
fsys := afero.NewMemMapFs()
9394
require.NoError(t, utils.InitConfig(utils.InitParams{ProjectId: "test"}, fsys))

internal/utils/deno.go

Lines changed: 39 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,20 @@ import (
66
"context"
77
"crypto/sha256"
88
"embed"
9-
"encoding/json"
109
"fmt"
1110
"io"
1211
"io/fs"
1312
"net/http"
1413
"os"
1514
"os/exec"
15+
"path"
1616
"path/filepath"
1717
"runtime"
1818
"strings"
1919

2020
"github.com/go-errors/errors"
2121
"github.com/spf13/afero"
22-
"github.com/tidwall/jsonc"
22+
"github.com/supabase/cli/pkg/function"
2323
)
2424

2525
var (
@@ -197,7 +197,6 @@ func CopyDenoScripts(ctx context.Context, fsys afero.Fs) (*DenoScriptDir, error)
197197

198198
return nil
199199
})
200-
201200
if err != nil {
202201
return nil, err
203202
}
@@ -210,87 +209,57 @@ func CopyDenoScripts(ctx context.Context, fsys afero.Fs) (*DenoScriptDir, error)
210209
return &sd, nil
211210
}
212211

213-
type ImportMap struct {
214-
Imports map[string]string `json:"imports"`
215-
Scopes map[string]map[string]string `json:"scopes"`
216-
}
217-
218-
func NewImportMap(absJsonPath string, fsys afero.Fs) (*ImportMap, error) {
219-
data, err := afero.ReadFile(fsys, absJsonPath)
212+
func newImportMap(relJsonPath string, fsys afero.Fs) (function.ImportMap, error) {
213+
var result function.ImportMap
214+
if len(relJsonPath) == 0 {
215+
return result, nil
216+
}
217+
data, err := afero.ReadFile(fsys, relJsonPath)
220218
if err != nil {
221-
return nil, errors.Errorf("failed to load import map: %w", err)
219+
return result, errors.Errorf("failed to load import map: %w", err)
222220
}
223-
result := ImportMap{}
224221
if err := result.Parse(data); err != nil {
225-
return nil, err
226-
}
227-
// Resolve all paths relative to current file
228-
for k, v := range result.Imports {
229-
result.Imports[k] = resolveHostPath(absJsonPath, v, fsys)
222+
return result, err
230223
}
231-
for module, mapping := range result.Scopes {
232-
for k, v := range mapping {
233-
result.Scopes[module][k] = resolveHostPath(absJsonPath, v, fsys)
234-
}
235-
}
236-
return &result, nil
237-
}
238-
239-
func (m *ImportMap) Parse(data []byte) error {
240-
data = jsonc.ToJSONInPlace(data)
241-
decoder := json.NewDecoder(bytes.NewReader(data))
242-
if err := decoder.Decode(&m); err != nil {
243-
return errors.Errorf("failed to parse import map: %w", err)
224+
unixPath := filepath.ToSlash(relJsonPath)
225+
if err := result.Resolve(unixPath, afero.NewIOFS(fsys)); err != nil {
226+
return result, err
244227
}
245-
return nil
228+
return result, nil
246229
}
247230

248-
func resolveHostPath(jsonPath, hostPath string, fsys afero.Fs) string {
249-
// Leave absolute paths unchanged
250-
if filepath.IsAbs(hostPath) {
251-
return hostPath
231+
func BindHostModules(cwd, relEntrypointPath, relImportMapPath string, fsys afero.Fs) ([]string, error) {
232+
importMap, err := newImportMap(relImportMapPath, fsys)
233+
if err != nil {
234+
return nil, err
252235
}
253-
resolved := filepath.Join(filepath.Dir(jsonPath), hostPath)
254-
if exists, err := afero.Exists(fsys, resolved); !exists {
255-
// Leave URLs unchanged
236+
var modules []string
237+
// Resolving all Import Graph
238+
addModule := func(unixPath string, w io.Writer) error {
239+
hostPath := filepath.FromSlash(unixPath)
240+
if path.IsAbs(unixPath) {
241+
hostPath = filepath.VolumeName(cwd) + hostPath
242+
} else {
243+
hostPath = filepath.Join(cwd, hostPath)
244+
}
245+
f, err := fsys.Open(hostPath)
256246
if err != nil {
257-
logger := GetDebugLogger()
258-
fmt.Fprintln(logger, err)
247+
return errors.Errorf("failed to read file: %w", err)
259248
}
260-
return hostPath
261-
}
262-
// Directory imports need to be suffixed with /
263-
// Ref: https://deno.com/[email protected]/basics/import_maps
264-
if strings.HasSuffix(hostPath, string(filepath.Separator)) {
265-
resolved += string(filepath.Separator)
266-
}
267-
return resolved
268-
}
269-
270-
func (m *ImportMap) BindHostModules() []string {
271-
hostFuncDir, err := filepath.Abs(FunctionsDir)
272-
if err != nil {
273-
logger := GetDebugLogger()
274-
fmt.Fprintln(logger, err)
275-
}
276-
binds := []string{}
277-
for _, hostPath := range m.Imports {
278-
if !filepath.IsAbs(hostPath) || strings.HasPrefix(hostPath, hostFuncDir) {
279-
continue
249+
defer f.Close()
250+
if _, err := io.Copy(w, f); err != nil {
251+
return errors.Errorf("failed to copy file content: %w", err)
280252
}
281253
dockerPath := ToDockerPath(hostPath)
282-
binds = append(binds, hostPath+":"+dockerPath+":ro")
254+
modules = append(modules, hostPath+":"+dockerPath+":ro")
255+
return nil
283256
}
284-
for _, mapping := range m.Scopes {
285-
for _, hostPath := range mapping {
286-
if !filepath.IsAbs(hostPath) || strings.HasPrefix(hostPath, hostFuncDir) {
287-
continue
288-
}
289-
dockerPath := ToDockerPath(hostPath)
290-
binds = append(binds, hostPath+":"+dockerPath+":ro")
291-
}
257+
unixPath := filepath.ToSlash(relEntrypointPath)
258+
if err := importMap.WalkImportPaths(unixPath, addModule); err != nil {
259+
return nil, err
292260
}
293-
return binds
261+
// TODO: support scopes
262+
return modules, nil
294263
}
295264

296265
func ToDockerPath(absHostPath string) string {

internal/utils/deno_test.go

Lines changed: 20 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ func TestResolveImports(t *testing.T) {
3131
require.NoError(t, fsys.Mkdir(filepath.Join(cwd, DbTestsDir), 0755))
3232
require.NoError(t, fsys.Mkdir(filepath.Join(cwd, FunctionsDir, "child"), 0755))
3333
// Run test
34-
resolved, err := NewImportMap(jsonPath, fsys)
34+
resolved, err := newImportMap(jsonPath, fsys)
3535
// Check error
3636
assert.NoError(t, err)
3737
assert.Equal(t, "/tmp/", resolved.Imports["abs/"])
@@ -53,7 +53,7 @@ func TestResolveImports(t *testing.T) {
5353
fsys := afero.NewMemMapFs()
5454
require.NoError(t, afero.WriteFile(fsys, FallbackImportMapPath, importMap, 0644))
5555
// Run test
56-
resolved, err := NewImportMap(FallbackImportMapPath, fsys)
56+
resolved, err := newImportMap(FallbackImportMapPath, fsys)
5757
// Check error
5858
assert.NoError(t, err)
5959
assert.Equal(t, "https://deno.land", resolved.Scopes["my-scope"]["my-mod"])
@@ -62,37 +62,27 @@ func TestResolveImports(t *testing.T) {
6262

6363
func TestBindModules(t *testing.T) {
6464
t.Run("binds docker imports", func(t *testing.T) {
65-
cwd, err := os.Getwd()
66-
require.NoError(t, err)
67-
importMap := ImportMap{
68-
Imports: map[string]string{
69-
"abs/": "/tmp/",
70-
"root": cwd + "/common",
71-
"parent": cwd + "/supabase/tests",
72-
"child": cwd + "/supabase/functions/child/",
73-
},
74-
}
65+
fsys := afero.NewMemMapFs()
66+
entrypoint := `import "https://deno.land"
67+
import "/tmp/index.ts"
68+
import "../common/index.ts"
69+
import "../../../supabase/tests/index.ts"
70+
import "./child/index.ts"`
71+
require.NoError(t, WriteFile("/app/supabase/functions/hello/index.ts", []byte(entrypoint), fsys))
72+
require.NoError(t, WriteFile("/tmp/index.ts", []byte{}, fsys))
73+
require.NoError(t, WriteFile("/app/supabase/functions/common/index.ts", []byte{}, fsys))
74+
require.NoError(t, WriteFile("/app/supabase/tests/index.ts", []byte{}, fsys))
75+
require.NoError(t, WriteFile("/app/supabase/functions/hello/child/index.ts", []byte{}, fsys))
7576
// Run test
76-
mods := importMap.BindHostModules()
77+
mods, err := BindHostModules("/app", "supabase/functions/hello/index.ts", "", fsys)
7778
// Check error
79+
assert.NoError(t, err)
7880
assert.ElementsMatch(t, mods, []string{
79-
"/tmp/:/tmp/:ro",
80-
cwd + "/common:" + cwd + "/common:ro",
81-
cwd + "/supabase/tests:" + cwd + "/supabase/tests:ro",
81+
"/app/supabase/functions/hello/index.ts:/app/supabase/functions/hello/index.ts:ro",
82+
"/tmp/index.ts:/tmp/index.ts:ro",
83+
"/app/supabase/functions/common/index.ts:/app/supabase/functions/common/index.ts:ro",
84+
"/app/supabase/tests/index.ts:/app/supabase/tests/index.ts:ro",
85+
"/app/supabase/functions/hello/child/index.ts:/app/supabase/functions/hello/child/index.ts:ro",
8286
})
8387
})
84-
85-
t.Run("binds docker scopes", func(t *testing.T) {
86-
importMap := ImportMap{
87-
Scopes: map[string]map[string]string{
88-
"my-scope": {
89-
"my-mod": "https://deno.land",
90-
},
91-
},
92-
}
93-
// Run test
94-
mods := importMap.BindHostModules()
95-
// Check error
96-
assert.Empty(t, mods)
97-
})
9888
}

pkg/function/deploy.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ func writeForm(form *multipart.Writer, meta api.FunctionDeployMetadata, fsys fs.
199199
return err
200200
}
201201
}
202-
return walkImportPaths(meta.EntrypointPath, importMap, addFile)
202+
return importMap.WalkImportPaths(meta.EntrypointPath, addFile)
203203
}
204204

205205
type ImportMap struct {
@@ -254,7 +254,7 @@ func resolveHostPath(jsonPath, hostPath string, fsys fs.FS) string {
254254
// Ref: https://regex101.com/r/DfBdJA/1
255255
var importPathPattern = regexp.MustCompile(`(?i)(?:import|export)\s+(?:{[^{}]+}|.*?)\s*(?:from)?\s*['"](.*?)['"]|import\(\s*['"](.*?)['"]\)`)
256256

257-
func walkImportPaths(srcPath string, importMap ImportMap, readFile func(curr string, w io.Writer) error) error {
257+
func (importMap *ImportMap) WalkImportPaths(srcPath string, readFile func(curr string, w io.Writer) error) error {
258258
seen := map[string]struct{}{}
259259
// DFS because it's more efficient to pop from end of array
260260
q := make([]string, 1)
@@ -294,7 +294,8 @@ func walkImportPaths(srcPath string, importMap ImportMap, readFile func(curr str
294294
substituted = true
295295
}
296296
}
297-
// Ignore URLs and directories
297+
// Ignore URLs and directories, assuming no sloppy imports
298+
// https://github.com/denoland/deno/issues/2506#issuecomment-2727635545
298299
if len(path.Ext(mod)) == 0 {
299300
continue
300301
}

0 commit comments

Comments
 (0)