diff --git a/pkg/function/deploy.go b/pkg/function/deploy.go index 3eef79284a..3a2f35d0ce 100644 --- a/pkg/function/deploy.go +++ b/pkg/function/deploy.go @@ -163,6 +163,9 @@ func writeForm(form *multipart.Writer, meta api.FunctionDeployMetadata, fsys fs. if err := importMap.Parse(data); err != nil { return err } + if err := importMap.Resolve(imPath, fsys); err != nil { + return err + } // TODO: replace with addFile once edge runtime supports jsonc fmt.Fprintf(os.Stderr, "Uploading asset (%s): %s\n", *meta.Name, imPath) f, err := form.CreateFormFile("file", imPath) @@ -201,6 +204,41 @@ func (m *ImportMap) Parse(data []byte) error { return nil } +func (m *ImportMap) Resolve(imPath string, fsys fs.FS) error { + // Resolve all paths relative to current file + for k, v := range m.Imports { + m.Imports[k] = resolveHostPath(imPath, v, fsys) + } + for module, mapping := range m.Scopes { + for k, v := range mapping { + m.Scopes[module][k] = resolveHostPath(imPath, v, fsys) + } + } + return nil +} + +func resolveHostPath(jsonPath, hostPath string, fsys fs.FS) string { + // Leave absolute paths unchanged + if path.IsAbs(hostPath) { + return hostPath + } + resolved := path.Join(path.Dir(jsonPath), hostPath) + if _, err := fs.Stat(fsys, filepath.FromSlash(resolved)); err != nil { + // Leave URLs unchanged + return hostPath + } + // Directory imports need to be suffixed with / + // Ref: https://deno.com/manual@v1.33.0/basics/import_maps + if strings.HasSuffix(hostPath, "/") { + resolved += "/" + } + // Relative imports must be prefixed with ./ or ../ + if !path.IsAbs(resolved) { + resolved = "./" + resolved + } + return resolved +} + // Ref: https://regex101.com/r/DfBdJA/1 var importPathPattern = regexp.MustCompile(`(?i)(?:import|export)\s+(?:{[^{}]+}|.*?)\s*(?:from)?\s*['"](.*?)['"]|import\(\s*['"](.*?)['"]\)`) @@ -237,22 +275,35 @@ func walkImportPaths(srcPath string, importMap ImportMap, readFile func(curr str } mod = strings.TrimSpace(mod) // Substitute kv from import map + substituted := false for k, v := range importMap.Imports { if strings.HasPrefix(mod, k) { mod = v + mod[len(k):] + substituted = true } } - // Deno import path must begin with these prefixes - if strings.HasPrefix(mod, "./") || strings.HasPrefix(mod, "../") { - mod = path.Join(path.Dir(curr), mod) - } else if !strings.HasPrefix(mod, "/") { + // Ignore URLs and directories + if len(path.Ext(mod)) == 0 { continue } - if len(path.Ext(mod)) > 0 { - // Cleans import path to help detect duplicates - q = append(q, path.Clean(mod)) + // Deno import path must begin with one of these prefixes + if !isRelPath(mod) && !isAbsPath(mod) { + continue } + if isRelPath(mod) && !substituted { + mod = path.Join(path.Dir(curr), mod) + } + // Cleans import path to help detect duplicates + q = append(q, path.Clean(mod)) } } return nil } + +func isRelPath(mod string) bool { + return strings.HasPrefix(mod, "./") || strings.HasPrefix(mod, "../") +} + +func isAbsPath(mod string) bool { + return strings.HasPrefix(mod, "/") +} diff --git a/pkg/function/deploy_test.go b/pkg/function/deploy_test.go index 424478aa34..7927afd772 100644 --- a/pkg/function/deploy_test.go +++ b/pkg/function/deploy_test.go @@ -49,8 +49,7 @@ func TestImportPaths(t *testing.T) { fsys.On("ReadFile", "testdata/modules/imports.ts").Once() fsys.On("ReadFile", "testdata/geometries/Geometries.js").Once() // Run test - im := ImportMap{} - err := walkImportPaths("testdata/modules/imports.ts", im, fsys.ReadFile) + err := walkImportPaths("testdata/modules/imports.ts", ImportMap{}, fsys.ReadFile) // Check error assert.NoError(t, err) fsys.AssertExpectations(t) @@ -65,10 +64,33 @@ func TestImportPaths(t *testing.T) { fsys.On("ReadFile", "testdata/shared/whatever.ts").Once() fsys.On("ReadFile", "testdata/shared/mod.ts").Once() fsys.On("ReadFile", "testdata/nested/index.ts").Once() - // Run test + // Setup deno.json im := ImportMap{Imports: map[string]string{ "module-name/": "../shared/", }} + assert.NoError(t, im.Resolve("testdata/modules/deno.json", testImports)) + // Run test + err := walkImportPaths("testdata/modules/imports.ts", im, fsys.ReadFile) + // Check error + assert.NoError(t, err) + fsys.AssertExpectations(t) + }) + + t.Run("resolves legacy import map", func(t *testing.T) { + // Setup in-memory fs + fsys := MockFS{} + fsys.On("ReadFile", "/modules/my-module.ts").Once() + fsys.On("ReadFile", "testdata/modules/imports.ts").Once() + fsys.On("ReadFile", "testdata/geometries/Geometries.js").Once() + fsys.On("ReadFile", "testdata/shared/whatever.ts").Once() + fsys.On("ReadFile", "testdata/shared/mod.ts").Once() + fsys.On("ReadFile", "testdata/nested/index.ts").Once() + // Setup legacy import map + im := ImportMap{Imports: map[string]string{ + "module-name/": "./shared/", + }} + assert.NoError(t, im.Resolve("testdata/import_map.json", testImports)) + // Run test err := walkImportPaths("testdata/modules/imports.ts", im, fsys.ReadFile) // Check error assert.NoError(t, err) diff --git a/pkg/function/testdata/nested/deno.json b/pkg/function/testdata/nested/deno.json index 0967ef424b..bfba6fbb2c 100644 --- a/pkg/function/testdata/nested/deno.json +++ b/pkg/function/testdata/nested/deno.json @@ -1 +1,5 @@ -{} +{ + "imports": { + "module": "jsr:@supabase/functions-js/edge-runtime.d.ts" + } +} diff --git a/pkg/function/testdata/nested/index.ts b/pkg/function/testdata/nested/index.ts index e69de29bb2..b0539a467f 100644 --- a/pkg/function/testdata/nested/index.ts +++ b/pkg/function/testdata/nested/index.ts @@ -0,0 +1 @@ +import "module"; diff --git a/pkg/function/testdata/writes_import_map.form b/pkg/function/testdata/writes_import_map.form index 75df843f70..174a55f6b0 100644 --- a/pkg/function/testdata/writes_import_map.form +++ b/pkg/function/testdata/writes_import_map.form @@ -7,7 +7,11 @@ Content-Disposition: form-data; name="metadata" Content-Disposition: form-data; name="file"; filename="testdata/nested/deno.json" Content-Type: application/octet-stream -{} +{ + "imports": { + "module": "jsr:@supabase/functions-js/edge-runtime.d.ts" + } +} --test Content-Disposition: form-data; name="file"; filename="testdata/geometries/Geometries.js" @@ -18,3 +22,4 @@ Content-Type: application/octet-stream Content-Disposition: form-data; name="file"; filename="testdata/nested/index.ts" Content-Type: application/octet-stream +import "module";