• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

supabase / cli / 14271020172

04 Apr 2025 05:37PM UTC coverage: 51.181% (+0.09%) from 51.096%
14271020172

push

github

web-flow
fix: resolve legacy import map (#3395)

38 of 45 new or added lines in 1 file covered. (84.44%)

2 existing lines in 1 file now uncovered.

6999 of 13675 relevant lines covered (51.18%)

184.65 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

76.62
/pkg/function/deploy.go
1
package function
2

3
import (
4
        "bytes"
5
        "context"
6
        "encoding/json"
7
        "fmt"
8
        "io"
9
        "io/fs"
10
        "mime/multipart"
11
        "os"
12
        "path"
13
        "path/filepath"
14
        "regexp"
15
        "strings"
16

17
        "github.com/go-errors/errors"
18
        "github.com/supabase/cli/pkg/api"
19
        "github.com/supabase/cli/pkg/cast"
20
        "github.com/supabase/cli/pkg/config"
21
        "github.com/supabase/cli/pkg/queue"
22
        "github.com/tidwall/jsonc"
23
)
24

25
var ErrNoDeploy = errors.New("All Functions are up to date.")
26

27
func (s *EdgeRuntimeAPI) Deploy(ctx context.Context, functionConfig config.FunctionConfig, fsys fs.FS) error {
3✔
28
        if s.eszip != nil {
3✔
29
                return s.UpsertFunctions(ctx, functionConfig)
×
30
        }
×
31
        var toDeploy []api.FunctionDeployMetadata
3✔
32
        for slug, fc := range functionConfig {
7✔
33
                if !fc.Enabled {
4✔
34
                        fmt.Fprintln(os.Stderr, "Skipped deploying Function:", slug)
×
35
                        continue
×
36
                }
37
                meta := api.FunctionDeployMetadata{
4✔
38
                        Name:           &slug,
4✔
39
                        EntrypointPath: filepath.ToSlash(fc.Entrypoint),
4✔
40
                        ImportMapPath:  cast.Ptr(filepath.ToSlash(fc.ImportMap)),
4✔
41
                        VerifyJwt:      &fc.VerifyJWT,
4✔
42
                }
4✔
43
                files := make([]string, len(fc.StaticFiles))
4✔
44
                for i, sf := range fc.StaticFiles {
4✔
45
                        files[i] = filepath.ToSlash(sf)
×
46
                }
×
47
                meta.StaticPatterns = &files
4✔
48
                toDeploy = append(toDeploy, meta)
4✔
49
        }
50
        if len(toDeploy) == 0 {
3✔
51
                return errors.New(ErrNoDeploy)
×
52
        } else if len(toDeploy) == 1 {
5✔
53
                param := api.V1DeployAFunctionParams{Slug: toDeploy[0].Name}
2✔
54
                _, err := s.upload(ctx, param, toDeploy[0], fsys)
2✔
55
                return err
2✔
56
        }
2✔
57
        return s.bulkUpload(ctx, toDeploy, fsys)
1✔
58
}
59

60
func (s *EdgeRuntimeAPI) bulkUpload(ctx context.Context, toDeploy []api.FunctionDeployMetadata, fsys fs.FS) error {
1✔
61
        jq := queue.NewJobQueue(s.maxJobs)
1✔
62
        toUpdate := make([]api.BulkUpdateFunctionBody, len(toDeploy))
1✔
63
        for i, meta := range toDeploy {
3✔
64
                fmt.Fprintln(os.Stderr, "Deploying Function:", *meta.Name)
2✔
65
                param := api.V1DeployAFunctionParams{
2✔
66
                        Slug:       meta.Name,
2✔
67
                        BundleOnly: cast.Ptr(true),
2✔
68
                }
2✔
69
                bundle := func() error {
4✔
70
                        resp, err := s.upload(ctx, param, meta, fsys)
2✔
71
                        if err != nil {
2✔
72
                                return err
×
73
                        }
×
74
                        toUpdate[i].Id = resp.Id
2✔
75
                        toUpdate[i].Name = resp.Name
2✔
76
                        toUpdate[i].Slug = resp.Slug
2✔
77
                        toUpdate[i].Version = resp.Version
2✔
78
                        toUpdate[i].EntrypointPath = resp.EntrypointPath
2✔
79
                        toUpdate[i].ImportMap = resp.ImportMap
2✔
80
                        toUpdate[i].ImportMapPath = resp.ImportMapPath
2✔
81
                        toUpdate[i].VerifyJwt = resp.VerifyJwt
2✔
82
                        toUpdate[i].Status = api.BulkUpdateFunctionBodyStatus(resp.Status)
2✔
83
                        toUpdate[i].CreatedAt = resp.CreatedAt
2✔
84
                        return nil
2✔
85
                }
86
                if err := jq.Put(bundle); err != nil {
2✔
87
                        return err
×
88
                }
×
89
        }
90
        if err := jq.Collect(); err != nil {
1✔
91
                return err
×
92
        }
×
93
        if resp, err := s.client.V1BulkUpdateFunctionsWithResponse(ctx, s.project, toUpdate); err != nil {
1✔
94
                return errors.Errorf("failed to bulk update: %w", err)
×
95
        } else if resp.JSON200 == nil {
1✔
96
                return errors.Errorf("unexpected bulk update status %d: %s", resp.StatusCode(), string(resp.Body))
×
97
        }
×
98
        return nil
1✔
99
}
100

101
func (s *EdgeRuntimeAPI) upload(ctx context.Context, param api.V1DeployAFunctionParams, meta api.FunctionDeployMetadata, fsys fs.FS) (*api.DeployFunctionResponse, error) {
4✔
102
        body, w := io.Pipe()
4✔
103
        form := multipart.NewWriter(w)
4✔
104
        ctx, cancel := context.WithCancelCause(ctx)
4✔
105
        go func() {
8✔
106
                defer w.Close()
4✔
107
                defer form.Close()
4✔
108
                if err := writeForm(form, meta, fsys); err != nil {
4✔
109
                        // Since we are streaming files to the POST request body, any errors
×
110
                        // should be propagated to the request context to cancel the upload.
×
111
                        cancel(err)
×
112
                }
×
113
        }()
114
        resp, err := s.client.V1DeployAFunctionWithBodyWithResponse(ctx, s.project, &param, form.FormDataContentType(), body)
4✔
115
        if cause := context.Cause(ctx); cause != ctx.Err() {
4✔
116
                return nil, cause
×
117
        } else if err != nil {
5✔
118
                return nil, errors.Errorf("failed to deploy function: %w", err)
1✔
119
        } else if resp.JSON201 == nil {
4✔
120
                return nil, errors.Errorf("unexpected deploy status %d: %s", resp.StatusCode(), string(resp.Body))
×
121
        }
×
122
        return resp.JSON201, nil
3✔
123
}
124

125
func writeForm(form *multipart.Writer, meta api.FunctionDeployMetadata, fsys fs.FS) error {
7✔
126
        m, err := form.CreateFormField("metadata")
7✔
127
        if err != nil {
7✔
128
                return errors.Errorf("failed to create metadata: %w", err)
×
129
        }
×
130
        enc := json.NewEncoder(m)
3✔
131
        if err := enc.Encode(meta); err != nil {
3✔
132
                return errors.Errorf("failed to encode metadata: %w", err)
×
133
        }
×
134
        addFile := func(srcPath string, w io.Writer) error {
6✔
135
                f, err := fsys.Open(filepath.FromSlash(srcPath))
3✔
136
                if err != nil {
3✔
137
                        return errors.Errorf("failed to read file: %w", err)
×
138
                }
×
139
                defer f.Close()
3✔
140
                if fi, err := f.Stat(); err != nil {
3✔
141
                        return errors.Errorf("failed to stat file: %w", err)
×
142
                } else if fi.IsDir() {
4✔
143
                        return errors.New("file path is a directory: " + srcPath)
1✔
144
                }
1✔
145
                fmt.Fprintf(os.Stderr, "Uploading asset (%s): %s\n", *meta.Name, srcPath)
2✔
146
                r := io.TeeReader(f, w)
2✔
147
                dst, err := form.CreateFormFile("file", srcPath)
2✔
148
                if err != nil {
2✔
149
                        return errors.Errorf("failed to create form: %w", err)
×
150
                }
×
151
                if _, err := io.Copy(dst, r); err != nil {
2✔
152
                        return errors.Errorf("failed to write form: %w", err)
×
153
                }
×
154
                return nil
2✔
155
        }
156
        // Add import map
157
        importMap := ImportMap{}
3✔
158
        if imPath := cast.Val(meta.ImportMapPath, ""); len(imPath) > 0 {
5✔
159
                data, err := fs.ReadFile(fsys, filepath.FromSlash(imPath))
2✔
160
                if err != nil {
3✔
161
                        return errors.Errorf("failed to load import map: %w", err)
1✔
162
                }
1✔
163
                if err := importMap.Parse(data); err != nil {
1✔
164
                        return err
×
165
                }
×
166
                if err := importMap.Resolve(imPath, fsys); err != nil {
1✔
NEW
167
                        return err
×
NEW
168
                }
×
169
                // TODO: replace with addFile once edge runtime supports jsonc
170
                fmt.Fprintf(os.Stderr, "Uploading asset (%s): %s\n", *meta.Name, imPath)
1✔
171
                f, err := form.CreateFormFile("file", imPath)
1✔
172
                if err != nil {
1✔
173
                        return errors.Errorf("failed to create import map: %w", err)
×
174
                }
×
175
                if _, err := f.Write(data); err != nil {
1✔
176
                        return errors.Errorf("failed to write import map: %w", err)
×
177
                }
×
178
        }
179
        // Add static files
180
        patterns := config.Glob(cast.Val(meta.StaticPatterns, []string{}))
2✔
181
        files, err := patterns.Files(fsys)
2✔
182
        if err != nil {
2✔
183
                fmt.Fprintln(os.Stderr, "WARN:", err)
×
184
        }
×
185
        for _, sfPath := range files {
4✔
186
                if err := addFile(sfPath, io.Discard); err != nil {
3✔
187
                        return err
1✔
188
                }
1✔
189
        }
190
        return walkImportPaths(meta.EntrypointPath, importMap, addFile)
1✔
191
}
192

193
type ImportMap struct {
194
        Imports map[string]string            `json:"imports"`
195
        Scopes  map[string]map[string]string `json:"scopes"`
196
}
197

198
func (m *ImportMap) Parse(data []byte) error {
1✔
199
        data = jsonc.ToJSONInPlace(data)
1✔
200
        decoder := json.NewDecoder(bytes.NewReader(data))
1✔
201
        if err := decoder.Decode(&m); err != nil {
1✔
202
                return errors.Errorf("failed to parse import map: %w", err)
×
203
        }
×
204
        return nil
1✔
205
}
206

207
func (m *ImportMap) Resolve(imPath string, fsys fs.FS) error {
3✔
208
        // Resolve all paths relative to current file
3✔
209
        for k, v := range m.Imports {
6✔
210
                m.Imports[k] = resolveHostPath(imPath, v, fsys)
3✔
211
        }
3✔
212
        for module, mapping := range m.Scopes {
3✔
NEW
213
                for k, v := range mapping {
×
NEW
214
                        m.Scopes[module][k] = resolveHostPath(imPath, v, fsys)
×
NEW
215
                }
×
216
        }
217
        return nil
3✔
218
}
219

220
func resolveHostPath(jsonPath, hostPath string, fsys fs.FS) string {
3✔
221
        // Leave absolute paths unchanged
3✔
222
        if path.IsAbs(hostPath) {
3✔
NEW
223
                return hostPath
×
NEW
224
        }
×
225
        resolved := path.Join(path.Dir(jsonPath), hostPath)
3✔
226
        if _, err := fs.Stat(fsys, filepath.FromSlash(resolved)); err != nil {
4✔
227
                // Leave URLs unchanged
1✔
228
                return hostPath
1✔
229
        }
1✔
230
        // Directory imports need to be suffixed with /
231
        // Ref: https://deno.com/manual@v1.33.0/basics/import_maps
232
        if strings.HasSuffix(hostPath, "/") {
4✔
233
                resolved += "/"
2✔
234
        }
2✔
235
        // Relative imports must be prefixed with ./ or ../
236
        if !path.IsAbs(resolved) {
4✔
237
                resolved = "./" + resolved
2✔
238
        }
2✔
239
        return resolved
2✔
240
}
241

242
// Ref: https://regex101.com/r/DfBdJA/1
243
var importPathPattern = regexp.MustCompile(`(?i)(?:import|export)\s+(?:{[^{}]+}|.*?)\s*(?:from)?\s*['"](.*?)['"]|import\(\s*['"](.*?)['"]\)`)
244

245
func walkImportPaths(srcPath string, importMap ImportMap, readFile func(curr string, w io.Writer) error) error {
4✔
246
        seen := map[string]struct{}{}
4✔
247
        // DFS because it's more efficient to pop from end of array
4✔
248
        q := make([]string, 1)
4✔
249
        q[0] = srcPath
4✔
250
        for len(q) > 0 {
31✔
251
                curr := q[len(q)-1]
27✔
252
                q = q[:len(q)-1]
27✔
253
                // Assume no file is symlinked
27✔
254
                if _, ok := seen[curr]; ok {
38✔
255
                        continue
11✔
256
                }
257
                seen[curr] = struct{}{}
16✔
258
                // Read into memory for regex match later
16✔
259
                var buf bytes.Buffer
16✔
260
                if err := readFile(curr, &buf); errors.Is(err, os.ErrNotExist) {
21✔
261
                        fmt.Fprintln(os.Stderr, "WARN:", err)
5✔
262
                        continue
5✔
263
                } else if err != nil {
11✔
264
                        return err
×
265
                }
×
266
                // Traverse all modules imported by the current source file
267
                for _, matches := range importPathPattern.FindAllStringSubmatch(buf.String(), -1) {
168✔
268
                        if len(matches) < 3 {
157✔
269
                                continue
×
270
                        }
271
                        // Matches 'from' clause if present, else fallback to 'import'
272
                        mod := matches[1]
157✔
273
                        if len(mod) == 0 {
163✔
274
                                mod = matches[2]
6✔
275
                        }
6✔
276
                        mod = strings.TrimSpace(mod)
157✔
277
                        // Substitute kv from import map
157✔
278
                        substituted := false
157✔
279
                        for k, v := range importMap.Imports {
264✔
280
                                if strings.HasPrefix(mod, k) {
112✔
281
                                        mod = v + mod[len(k):]
5✔
282
                                        substituted = true
5✔
283
                                }
5✔
284
                        }
285
                        // Ignore URLs and directories
286
                        if len(path.Ext(mod)) == 0 {
288✔
287
                                continue
131✔
288
                        }
289
                        // Deno import path must begin with one of these prefixes
290
                        if !isRelPath(mod) && !isAbsPath(mod) {
29✔
291
                                continue
3✔
292
                        }
293
                        if isRelPath(mod) && !substituted {
33✔
294
                                mod = path.Join(path.Dir(curr), mod)
10✔
295
                        }
10✔
296
                        // Cleans import path to help detect duplicates
297
                        q = append(q, path.Clean(mod))
23✔
298
                }
299
        }
300
        return nil
4✔
301
}
302

303
func isRelPath(mod string) bool {
49✔
304
        return strings.HasPrefix(mod, "./") || strings.HasPrefix(mod, "../")
49✔
305
}
49✔
306

307
func isAbsPath(mod string) bool {
12✔
308
        return strings.HasPrefix(mod, "/")
12✔
309
}
12✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc