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

supabase / cli / 14340179993

08 Apr 2025 05:47PM UTC coverage: 51.131% (-0.005%) from 51.136%
14340179993

push

github

web-flow
fix: convert all paths to relative for deploy (#3403)

* fix: convert all paths to relative for deploy

* chore: move log statement to callback

6 of 11 new or added lines in 1 file covered. (54.55%)

2 existing lines in 1 file now uncovered.

7005 of 13700 relevant lines covered (51.13%)

184.31 hits per line

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

75.63
/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
        // Convert all paths in functions config to relative when using api deploy
32
        var toDeploy []api.FunctionDeployMetadata
3✔
33
        for slug, fc := range functionConfig {
7✔
34
                if !fc.Enabled {
4✔
35
                        fmt.Fprintln(os.Stderr, "Skipped deploying Function:", slug)
×
36
                        continue
×
37
                }
38
                meta := api.FunctionDeployMetadata{
4✔
39
                        Name:           &slug,
4✔
40
                        EntrypointPath: toRelPath(fc.Entrypoint),
4✔
41
                        ImportMapPath:  cast.Ptr(toRelPath(fc.ImportMap)),
4✔
42
                        VerifyJwt:      &fc.VerifyJWT,
4✔
43
                }
4✔
44
                files := make([]string, len(fc.StaticFiles))
4✔
45
                for i, sf := range fc.StaticFiles {
4✔
NEW
46
                        files[i] = toRelPath(sf)
×
47
                }
×
48
                meta.StaticPatterns = &files
4✔
49
                toDeploy = append(toDeploy, meta)
4✔
50
        }
51
        if len(toDeploy) == 0 {
3✔
52
                return errors.New(ErrNoDeploy)
×
53
        } else if len(toDeploy) == 1 {
5✔
54
                param := api.V1DeployAFunctionParams{Slug: toDeploy[0].Name}
2✔
55
                _, err := s.upload(ctx, param, toDeploy[0], fsys)
2✔
56
                return err
2✔
57
        }
2✔
58
        return s.bulkUpload(ctx, toDeploy, fsys)
1✔
59
}
60

61
func toRelPath(fp string) string {
8✔
62
        if filepath.IsAbs(fp) {
8✔
NEW
63
                if cwd, err := os.Getwd(); err == nil {
×
NEW
64
                        if relPath, err := filepath.Rel(cwd, fp); err == nil {
×
NEW
65
                                fp = relPath
×
NEW
66
                        }
×
67
                }
68
        }
69
        return filepath.ToSlash(fp)
8✔
70
}
71

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

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

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

205
type ImportMap struct {
206
        Imports map[string]string            `json:"imports"`
207
        Scopes  map[string]map[string]string `json:"scopes"`
208
}
209

210
func (m *ImportMap) Parse(data []byte) error {
1✔
211
        data = jsonc.ToJSONInPlace(data)
1✔
212
        decoder := json.NewDecoder(bytes.NewReader(data))
1✔
213
        if err := decoder.Decode(&m); err != nil {
1✔
214
                return errors.Errorf("failed to parse import map: %w", err)
×
215
        }
×
216
        return nil
1✔
217
}
218

219
func (m *ImportMap) Resolve(imPath string, fsys fs.FS) error {
3✔
220
        // Resolve all paths relative to current file
3✔
221
        for k, v := range m.Imports {
6✔
222
                m.Imports[k] = resolveHostPath(imPath, v, fsys)
3✔
223
        }
3✔
224
        for module, mapping := range m.Scopes {
3✔
225
                for k, v := range mapping {
×
226
                        m.Scopes[module][k] = resolveHostPath(imPath, v, fsys)
×
227
                }
×
228
        }
229
        return nil
3✔
230
}
231

232
func resolveHostPath(jsonPath, hostPath string, fsys fs.FS) string {
3✔
233
        // Leave absolute paths unchanged
3✔
234
        if path.IsAbs(hostPath) {
3✔
235
                return hostPath
×
236
        }
×
237
        resolved := path.Join(path.Dir(jsonPath), hostPath)
3✔
238
        if _, err := fs.Stat(fsys, filepath.FromSlash(resolved)); err != nil {
4✔
239
                // Leave URLs unchanged
1✔
240
                return hostPath
1✔
241
        }
1✔
242
        // Directory imports need to be suffixed with /
243
        // Ref: https://deno.com/manual@v1.33.0/basics/import_maps
244
        if strings.HasSuffix(hostPath, "/") {
4✔
245
                resolved += "/"
2✔
246
        }
2✔
247
        // Relative imports must be prefixed with ./ or ../
248
        if !path.IsAbs(resolved) {
4✔
249
                resolved = "./" + resolved
2✔
250
        }
2✔
251
        return resolved
2✔
252
}
253

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

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

315
func isRelPath(mod string) bool {
49✔
316
        return strings.HasPrefix(mod, "./") || strings.HasPrefix(mod, "../")
49✔
317
}
49✔
318

319
func isAbsPath(mod string) bool {
12✔
320
        return strings.HasPrefix(mod, "/")
12✔
321
}
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