Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 0 additions & 7 deletions internal/functions/deploy/bundle.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -127,13 +127,6 @@ func GetBindMounts(cwd, hostFuncDir, hostOutputDir, hostEntrypointPath, hostImpo
if err != nil {
return nil, err
}
if len(hostImportMapPath) > 0 {
if !filepath.IsAbs(hostImportMapPath) {
hostImportMapPath = filepath.Join(cwd, hostImportMapPath)
}
dockerImportMapPath := utils.ToDockerPath(hostImportMapPath)
modules = append(modules, hostImportMapPath+":"+dockerImportMapPath+":ro")
}
// Remove any duplicate mount points
for _, mod := range modules {
hostPath := strings.Split(mod, ":")[0]
Expand Down
35 changes: 12 additions & 23 deletions internal/utils/deno.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -209,31 +209,20 @@ func CopyDenoScripts(ctx context.Context, fsys afero.Fs) (*DenoScriptDir, error)
return &sd, nil
}

func newImportMap(relJsonPath string, fsys afero.Fs) (function.ImportMap, error) {
var result function.ImportMap
if len(relJsonPath) == 0 {
return result, nil
}
data, err := afero.ReadFile(fsys, relJsonPath)
if err != nil {
return result, errors.Errorf("failed to load import map: %w", err)
}
if err := result.Parse(data); err != nil {
return result, err
}
unixPath := filepath.ToSlash(relJsonPath)
if err := result.Resolve(unixPath, afero.NewIOFS(fsys)); err != nil {
return result, err
}
return result, nil
}

func BindHostModules(cwd, relEntrypointPath, relImportMapPath string, fsys afero.Fs) ([]string, error) {
importMap, err := newImportMap(relImportMapPath, fsys)
if err != nil {
return nil, err
}
var modules []string
bindModule := func(srcPath string, r io.Reader) error {
hostPath := filepath.Join(cwd, filepath.FromSlash(srcPath))
dockerPath := ToDockerPath(hostPath)
modules = append(modules, hostPath+":"+dockerPath+":ro")
return nil
}
importMap := function.ImportMap{}
if imPath := filepath.ToSlash(relImportMapPath); len(imPath) > 0 {
if err := importMap.LoadAsDeno(imPath, afero.NewIOFS(fsys), bindModule); err != nil {
return nil, err
}
}
// Resolving all Import Graph
addModule := func(unixPath string, w io.Writer) error {
hostPath := filepath.FromSlash(unixPath)
Expand Down
52 changes: 0 additions & 52 deletions internal/utils/deno_test.go
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,13 @@
package utils

import (
"os"
"path/filepath"
"testing"

"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestResolveImports(t *testing.T) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

praise

Nice to get them all centralized in a single place.

t.Run("resolves relative directory", func(t *testing.T) {
importMap := []byte(`{
"imports": {
"abs/": "/tmp/",
"root": "../../common",
"parent": "../tests",
"child": "child/",
"missing": "../missing"
}
}`)
// Setup in-memory fs
fsys := afero.NewMemMapFs()
cwd, err := os.Getwd()
require.NoError(t, err)
jsonPath := filepath.Join(cwd, FallbackImportMapPath)
require.NoError(t, afero.WriteFile(fsys, jsonPath, importMap, 0644))
require.NoError(t, fsys.Mkdir(filepath.Join(cwd, "common"), 0755))
require.NoError(t, fsys.Mkdir(filepath.Join(cwd, DbTestsDir), 0755))
require.NoError(t, fsys.Mkdir(filepath.Join(cwd, FunctionsDir, "child"), 0755))
// Run test
resolved, err := newImportMap(jsonPath, fsys)
// Check error
assert.NoError(t, err)
assert.Equal(t, "/tmp/", resolved.Imports["abs/"])
assert.Equal(t, cwd+"/common", resolved.Imports["root"])
assert.Equal(t, cwd+"/supabase/tests", resolved.Imports["parent"])
assert.Equal(t, cwd+"/supabase/functions/child/", resolved.Imports["child"])
assert.Equal(t, "../missing", resolved.Imports["missing"])
})

t.Run("resolves parent scopes", func(t *testing.T) {
importMap := []byte(`{
"scopes": {
"my-scope": {
"my-mod": "https://deno.land"
}
}
}`)
// Setup in-memory fs
fsys := afero.NewMemMapFs()
require.NoError(t, afero.WriteFile(fsys, FallbackImportMapPath, importMap, 0644))
// Run test
resolved, err := newImportMap(FallbackImportMapPath, fsys)
// Check error
assert.NoError(t, err)
assert.Equal(t, "https://deno.land", resolved.Scopes["my-scope"]["my-mod"])
})
}

func TestBindModules(t *testing.T) {
t.Run("binds docker imports", func(t *testing.T) {
fsys := afero.NewMemMapFs()
Expand Down
180 changes: 180 additions & 0 deletions pkg/function/deno.go
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
package function

import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/fs"
"os"
"path"
"path/filepath"
"regexp"
"strings"

"github.com/go-errors/errors"
"github.com/tidwall/jsonc"
)

type ImportMap struct {
Imports map[string]string `json:"imports"`
Scopes map[string]map[string]string `json:"scopes"`
// Fallback reference for deno.json
ImportMap string `json:"importMap"`
}

func (m *ImportMap) LoadAsDeno(imPath string, fsys fs.FS, opts ...func(string, io.Reader) error) error {
if err := m.Load(imPath, fsys, opts...); err != nil {
return err
}
if name := path.Base(imPath); isDeno(name) && m.IsReference() {
imPath = path.Join(path.Dir(imPath), m.ImportMap)
if err := m.Load(imPath, fsys, opts...); err != nil {
return err
}
}
return nil
}

func isDeno(name string) bool {
return strings.EqualFold(name, "deno.json") ||
strings.EqualFold(name, "deno.jsonc")
}

func (m *ImportMap) IsReference() bool {
// Ref: https://github.com/denoland/deno/blob/main/cli/schemas/config-file.v1.json#L273
return len(m.Imports) == 0 && len(m.Scopes) == 0 && len(m.ImportMap) > 0
}

func (m *ImportMap) Load(imPath string, fsys fs.FS, opts ...func(string, io.Reader) error) error {
data, err := fs.ReadFile(fsys, filepath.FromSlash(imPath))
if err != nil {
return errors.Errorf("failed to load import map: %w", err)
}
if err := m.Parse(data); err != nil {
return err
}
if err := m.Resolve(imPath, fsys); err != nil {
return err
}
for _, apply := range opts {
if err := apply(imPath, bytes.NewReader(data)); err != nil {
return err
}
}
return nil
}

func (m *ImportMap) Parse(data []byte) error {
data = jsonc.ToJSONInPlace(data)
decoder := json.NewDecoder(bytes.NewReader(data))
if err := decoder.Decode(&m); err != nil {
return errors.Errorf("failed to parse import map: %w", err)
}
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*['"](.*?)['"]\)`)

func (importMap *ImportMap) WalkImportPaths(srcPath string, readFile func(curr string, w io.Writer) error) error {
seen := map[string]struct{}{}
// DFS because it's more efficient to pop from end of array
q := make([]string, 1)
q[0] = srcPath
for len(q) > 0 {
curr := q[len(q)-1]
q = q[:len(q)-1]
// Assume no file is symlinked
if _, ok := seen[curr]; ok {
continue
}
seen[curr] = struct{}{}
// Read into memory for regex match later
var buf bytes.Buffer
if err := readFile(curr, &buf); errors.Is(err, os.ErrNotExist) {
fmt.Fprintln(os.Stderr, "WARN:", err)
continue
} else if err != nil {
return err
}
// Traverse all modules imported by the current source file
for _, matches := range importPathPattern.FindAllStringSubmatch(buf.String(), -1) {
if len(matches) < 3 {
continue
}
// Matches 'from' clause if present, else fallback to 'import'
mod := matches[1]
if len(mod) == 0 {
mod = matches[2]
}
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
}
}
// Ignore URLs and directories, assuming no sloppy imports
// https://github.com/denoland/deno/issues/2506#issuecomment-2727635545
if len(path.Ext(mod)) == 0 {
continue
}
Comment on lines +155 to +159

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note

Should we mention this in the Edge-Functions docs ?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup good idea. It's kind of niche feature though.

// 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, "/")
}
Loading