Skip to content

Commit b91727b

Browse files
committed
chore: add unit tests
1 parent d9b3838 commit b91727b

File tree

5 files changed

+381
-298
lines changed

5 files changed

+381
-298
lines changed

internal/utils/deno_test.go

Lines changed: 0 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,68 +1,13 @@
11
package utils
22

33
import (
4-
"os"
5-
"path/filepath"
64
"testing"
75

86
"github.com/spf13/afero"
97
"github.com/stretchr/testify/assert"
108
"github.com/stretchr/testify/require"
11-
"github.com/supabase/cli/pkg/function"
129
)
1310

14-
func TestResolveImports(t *testing.T) {
15-
t.Run("resolves relative directory", func(t *testing.T) {
16-
importMap := []byte(`{
17-
"imports": {
18-
"abs/": "/tmp/",
19-
"root": "../../common",
20-
"parent": "../tests",
21-
"child": "child/",
22-
"missing": "../missing"
23-
}
24-
}`)
25-
// Setup in-memory fs
26-
fsys := afero.NewMemMapFs()
27-
cwd, err := os.Getwd()
28-
require.NoError(t, err)
29-
jsonPath := filepath.Join(cwd, FallbackImportMapPath)
30-
require.NoError(t, afero.WriteFile(fsys, jsonPath, importMap, 0644))
31-
require.NoError(t, fsys.Mkdir(filepath.Join(cwd, "common"), 0755))
32-
require.NoError(t, fsys.Mkdir(filepath.Join(cwd, DbTestsDir), 0755))
33-
require.NoError(t, fsys.Mkdir(filepath.Join(cwd, FunctionsDir, "child"), 0755))
34-
// Run test
35-
resolved := function.ImportMap{}
36-
err = resolved.Load(jsonPath, afero.NewIOFS(fsys))
37-
// Check error
38-
assert.NoError(t, err)
39-
assert.Equal(t, "/tmp/", resolved.Imports["abs/"])
40-
assert.Equal(t, cwd+"/common", resolved.Imports["root"])
41-
assert.Equal(t, cwd+"/supabase/tests", resolved.Imports["parent"])
42-
assert.Equal(t, cwd+"/supabase/functions/child/", resolved.Imports["child"])
43-
assert.Equal(t, "../missing", resolved.Imports["missing"])
44-
})
45-
46-
t.Run("resolves parent scopes", func(t *testing.T) {
47-
importMap := []byte(`{
48-
"scopes": {
49-
"my-scope": {
50-
"my-mod": "https://deno.land"
51-
}
52-
}
53-
}`)
54-
// Setup in-memory fs
55-
fsys := afero.NewMemMapFs()
56-
require.NoError(t, afero.WriteFile(fsys, FallbackImportMapPath, importMap, 0644))
57-
// Run test
58-
resolved := function.ImportMap{}
59-
err := resolved.Load(FallbackImportMapPath, afero.NewIOFS(fsys))
60-
// Check error
61-
assert.NoError(t, err)
62-
assert.Equal(t, "https://deno.land", resolved.Scopes["my-scope"]["my-mod"])
63-
})
64-
}
65-
6611
func TestBindModules(t *testing.T) {
6712
t.Run("binds docker imports", func(t *testing.T) {
6813
fsys := afero.NewMemMapFs()

pkg/function/deno.go

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
package function
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"fmt"
7+
"io"
8+
"io/fs"
9+
"os"
10+
"path"
11+
"path/filepath"
12+
"regexp"
13+
"strings"
14+
15+
"github.com/go-errors/errors"
16+
"github.com/tidwall/jsonc"
17+
)
18+
19+
type ImportMap struct {
20+
Imports map[string]string `json:"imports"`
21+
Scopes map[string]map[string]string `json:"scopes"`
22+
// Fallback reference for deno.json
23+
ImportMap string `json:"importMap"`
24+
}
25+
26+
func (m *ImportMap) LoadAsDeno(imPath string, fsys fs.FS, opts ...func(string, io.Reader) error) error {
27+
if err := m.Load(imPath, fsys, opts...); err != nil {
28+
return err
29+
}
30+
if name := path.Base(imPath); isDeno(name) && m.IsReference() {
31+
imPath = path.Join(path.Dir(imPath), m.ImportMap)
32+
if err := m.Load(imPath, fsys, opts...); err != nil {
33+
return err
34+
}
35+
}
36+
return nil
37+
}
38+
39+
func isDeno(name string) bool {
40+
return strings.EqualFold(name, "deno.json") ||
41+
strings.EqualFold(name, "deno.jsonc")
42+
}
43+
44+
func (m *ImportMap) IsReference() bool {
45+
// Ref: https://github.com/denoland/deno/blob/main/cli/schemas/config-file.v1.json#L273
46+
return len(m.Imports) == 0 && len(m.Scopes) == 0 && len(m.ImportMap) > 0
47+
}
48+
49+
func (m *ImportMap) Load(imPath string, fsys fs.FS, opts ...func(string, io.Reader) error) error {
50+
data, err := fs.ReadFile(fsys, filepath.FromSlash(imPath))
51+
if err != nil {
52+
return errors.Errorf("failed to load import map: %w", err)
53+
}
54+
if err := m.Parse(data); err != nil {
55+
return err
56+
}
57+
if err := m.Resolve(imPath, fsys); err != nil {
58+
return err
59+
}
60+
for _, apply := range opts {
61+
if err := apply(imPath, bytes.NewReader(data)); err != nil {
62+
return err
63+
}
64+
}
65+
return nil
66+
}
67+
68+
func (m *ImportMap) Parse(data []byte) error {
69+
data = jsonc.ToJSONInPlace(data)
70+
decoder := json.NewDecoder(bytes.NewReader(data))
71+
if err := decoder.Decode(&m); err != nil {
72+
return errors.Errorf("failed to parse import map: %w", err)
73+
}
74+
return nil
75+
}
76+
77+
func (m *ImportMap) Resolve(imPath string, fsys fs.FS) error {
78+
// Resolve all paths relative to current file
79+
for k, v := range m.Imports {
80+
m.Imports[k] = resolveHostPath(imPath, v, fsys)
81+
}
82+
for module, mapping := range m.Scopes {
83+
for k, v := range mapping {
84+
m.Scopes[module][k] = resolveHostPath(imPath, v, fsys)
85+
}
86+
}
87+
return nil
88+
}
89+
90+
func resolveHostPath(jsonPath, hostPath string, fsys fs.FS) string {
91+
// Leave absolute paths unchanged
92+
if path.IsAbs(hostPath) {
93+
return hostPath
94+
}
95+
resolved := path.Join(path.Dir(jsonPath), hostPath)
96+
if _, err := fs.Stat(fsys, filepath.FromSlash(resolved)); err != nil {
97+
// Leave URLs unchanged
98+
return hostPath
99+
}
100+
// Directory imports need to be suffixed with /
101+
// Ref: https://deno.com/[email protected]/basics/import_maps
102+
if strings.HasSuffix(hostPath, "/") {
103+
resolved += "/"
104+
}
105+
// Relative imports must be prefixed with ./ or ../
106+
if !path.IsAbs(resolved) {
107+
resolved = "./" + resolved
108+
}
109+
return resolved
110+
}
111+
112+
// Ref: https://regex101.com/r/DfBdJA/1
113+
var importPathPattern = regexp.MustCompile(`(?i)(?:import|export)\s+(?:{[^{}]+}|.*?)\s*(?:from)?\s*['"](.*?)['"]|import\(\s*['"](.*?)['"]\)`)
114+
115+
func (importMap *ImportMap) WalkImportPaths(srcPath string, readFile func(curr string, w io.Writer) error) error {
116+
seen := map[string]struct{}{}
117+
// DFS because it's more efficient to pop from end of array
118+
q := make([]string, 1)
119+
q[0] = srcPath
120+
for len(q) > 0 {
121+
curr := q[len(q)-1]
122+
q = q[:len(q)-1]
123+
// Assume no file is symlinked
124+
if _, ok := seen[curr]; ok {
125+
continue
126+
}
127+
seen[curr] = struct{}{}
128+
// Read into memory for regex match later
129+
var buf bytes.Buffer
130+
if err := readFile(curr, &buf); errors.Is(err, os.ErrNotExist) {
131+
fmt.Fprintln(os.Stderr, "WARN:", err)
132+
continue
133+
} else if err != nil {
134+
return err
135+
}
136+
// Traverse all modules imported by the current source file
137+
for _, matches := range importPathPattern.FindAllStringSubmatch(buf.String(), -1) {
138+
if len(matches) < 3 {
139+
continue
140+
}
141+
// Matches 'from' clause if present, else fallback to 'import'
142+
mod := matches[1]
143+
if len(mod) == 0 {
144+
mod = matches[2]
145+
}
146+
mod = strings.TrimSpace(mod)
147+
// Substitute kv from import map
148+
substituted := false
149+
for k, v := range importMap.Imports {
150+
if strings.HasPrefix(mod, k) {
151+
mod = v + mod[len(k):]
152+
substituted = true
153+
}
154+
}
155+
// Ignore URLs and directories, assuming no sloppy imports
156+
// https://github.com/denoland/deno/issues/2506#issuecomment-2727635545
157+
if len(path.Ext(mod)) == 0 {
158+
continue
159+
}
160+
// Deno import path must begin with one of these prefixes
161+
if !isRelPath(mod) && !isAbsPath(mod) {
162+
continue
163+
}
164+
if isRelPath(mod) && !substituted {
165+
mod = path.Join(path.Dir(curr), mod)
166+
}
167+
// Cleans import path to help detect duplicates
168+
q = append(q, path.Clean(mod))
169+
}
170+
}
171+
return nil
172+
}
173+
174+
func isRelPath(mod string) bool {
175+
return strings.HasPrefix(mod, "./") || strings.HasPrefix(mod, "../")
176+
}
177+
178+
func isAbsPath(mod string) bool {
179+
return strings.HasPrefix(mod, "/")
180+
}

0 commit comments

Comments
 (0)