github.com/supabase/cli@v1.168.1/internal/utils/deno.go (about) 1 package utils 2 3 import ( 4 "archive/zip" 5 "bytes" 6 "context" 7 "crypto/sha256" 8 "embed" 9 "encoding/hex" 10 "encoding/json" 11 "fmt" 12 "io" 13 "io/fs" 14 "net/http" 15 "os" 16 "os/exec" 17 "path" 18 "path/filepath" 19 "runtime" 20 "strings" 21 22 "github.com/go-errors/errors" 23 "github.com/spf13/afero" 24 ) 25 26 var ( 27 //go:embed denos/* 28 denoEmbedDir embed.FS 29 // Used by unit tests 30 DenoPathOverride string 31 ) 32 33 const ( 34 DockerDenoDir = "/home/deno" 35 DockerModsDir = DockerDenoDir + "/modules" 36 DockerFuncDirPath = DockerDenoDir + "/functions" 37 ) 38 39 func GetDenoPath() (string, error) { 40 if len(DenoPathOverride) > 0 { 41 return DenoPathOverride, nil 42 } 43 home, err := os.UserHomeDir() 44 if err != nil { 45 return "", err 46 } 47 denoBinName := "deno" 48 if runtime.GOOS == "windows" { 49 denoBinName = "deno.exe" 50 } 51 denoPath := filepath.Join(home, ".supabase", denoBinName) 52 return denoPath, nil 53 } 54 55 func InstallOrUpgradeDeno(ctx context.Context, fsys afero.Fs) error { 56 denoPath, err := GetDenoPath() 57 if err != nil { 58 return err 59 } 60 61 if _, err := fsys.Stat(denoPath); err == nil { 62 // Upgrade Deno. 63 cmd := exec.CommandContext(ctx, denoPath, "upgrade", "--version", DenoVersion) 64 cmd.Stderr = os.Stderr 65 cmd.Stdout = os.Stdout 66 return cmd.Run() 67 } else if !errors.Is(err, os.ErrNotExist) { 68 return err 69 } 70 71 // Install Deno. 72 if err := MkdirIfNotExistFS(fsys, filepath.Dir(denoPath)); err != nil { 73 return err 74 } 75 76 // 1. Determine OS triple 77 assetFilename, err := getDenoAssetFileName() 78 if err != nil { 79 return err 80 } 81 assetRepo := "denoland/deno" 82 if runtime.GOOS == "linux" && runtime.GOARCH == "arm64" { 83 // TODO: version pin to official release once available https://github.com/denoland/deno/issues/1846 84 assetRepo = "LukeChannings/deno-arm64" 85 } 86 87 // 2. Download & install Deno binary. 88 { 89 assetUrl := fmt.Sprintf("https://github.com/%s/releases/download/v%s/%s", assetRepo, DenoVersion, assetFilename) 90 req, err := http.NewRequestWithContext(ctx, http.MethodGet, assetUrl, nil) 91 if err != nil { 92 return err 93 } 94 resp, err := http.DefaultClient.Do(req) 95 if err != nil { 96 return err 97 } 98 defer resp.Body.Close() 99 100 if resp.StatusCode != 200 { 101 return errors.New("Failed installing Deno binary.") 102 } 103 104 body, err := io.ReadAll(resp.Body) 105 if err != nil { 106 return err 107 } 108 109 r, err := zip.NewReader(bytes.NewReader(body), int64(len(body))) 110 // There should be only 1 file: the deno binary 111 if len(r.File) != 1 { 112 return err 113 } 114 denoContents, err := r.File[0].Open() 115 if err != nil { 116 return err 117 } 118 defer denoContents.Close() 119 120 denoBytes, err := io.ReadAll(denoContents) 121 if err != nil { 122 return err 123 } 124 125 if err := afero.WriteFile(fsys, denoPath, denoBytes, 0755); err != nil { 126 return err 127 } 128 } 129 130 return nil 131 } 132 133 func isScriptModified(fsys afero.Fs, destPath string, src []byte) (bool, error) { 134 dest, err := afero.ReadFile(fsys, destPath) 135 if err != nil { 136 if errors.Is(err, fs.ErrNotExist) { 137 return true, nil 138 } 139 return false, err 140 } 141 142 // compare the md5 checksum of src bytes with user's copy. 143 // if the checksums doesn't match, script is modified. 144 return sha256.Sum256(dest) != sha256.Sum256(src), nil 145 } 146 147 type DenoScriptDir struct { 148 ExtractPath string 149 BuildPath string 150 } 151 152 // Copy Deno scripts needed for function deploy and downloads, returning a DenoScriptDir struct or an error. 153 func CopyDenoScripts(ctx context.Context, fsys afero.Fs) (*DenoScriptDir, error) { 154 denoPath, err := GetDenoPath() 155 if err != nil { 156 return nil, err 157 } 158 159 denoDirPath := filepath.Dir(denoPath) 160 scriptDirPath := filepath.Join(denoDirPath, "denos") 161 162 // make the script directory if not exist 163 if err := MkdirIfNotExistFS(fsys, scriptDirPath); err != nil { 164 return nil, err 165 } 166 167 // copy embed files to script directory 168 err = fs.WalkDir(denoEmbedDir, "denos", func(path string, d fs.DirEntry, err error) error { 169 if err != nil { 170 return err 171 } 172 173 // skip copying the directory 174 if d.IsDir() { 175 return nil 176 } 177 178 destPath := filepath.Join(denoDirPath, path) 179 180 contents, err := fs.ReadFile(denoEmbedDir, path) 181 if err != nil { 182 return err 183 } 184 185 // check if the script should be copied 186 modified, err := isScriptModified(fsys, destPath, contents) 187 if err != nil { 188 return err 189 } 190 if !modified { 191 return nil 192 } 193 194 if err := afero.WriteFile(fsys, filepath.Join(denoDirPath, path), contents, 0666); err != nil { 195 return err 196 } 197 198 return nil 199 }) 200 201 if err != nil { 202 return nil, err 203 } 204 205 sd := DenoScriptDir{ 206 ExtractPath: filepath.Join(scriptDirPath, "extract.ts"), 207 BuildPath: filepath.Join(scriptDirPath, "build.ts"), 208 } 209 210 return &sd, nil 211 } 212 213 type ImportMap struct { 214 Imports map[string]string `json:"imports"` 215 Scopes map[string]map[string]string `json:"scopes"` 216 } 217 218 func NewImportMap(path string, fsys afero.Fs) (*ImportMap, error) { 219 contents, err := fsys.Open(path) 220 if err != nil { 221 return nil, errors.Errorf("failed to load import map: %w", err) 222 } 223 defer contents.Close() 224 return NewFromReader(contents) 225 } 226 227 func NewFromReader(r io.Reader) (*ImportMap, error) { 228 decoder := json.NewDecoder(r) 229 importMap := &ImportMap{} 230 if err := decoder.Decode(importMap); err != nil { 231 return nil, errors.Errorf("failed to parse import map: %w", err) 232 } 233 return importMap, nil 234 } 235 236 func (m *ImportMap) Resolve(fsys afero.Fs) ImportMap { 237 result := ImportMap{ 238 Imports: make(map[string]string, len(m.Imports)), 239 Scopes: make(map[string]map[string]string, len(m.Scopes)), 240 } 241 for k, v := range m.Imports { 242 result.Imports[k] = resolveHostPath(v, fsys) 243 } 244 for module, mapping := range m.Scopes { 245 result.Scopes[module] = map[string]string{} 246 for k, v := range mapping { 247 result.Scopes[module][k] = resolveHostPath(v, fsys) 248 } 249 } 250 return result 251 } 252 253 func (m *ImportMap) BindModules(resolved ImportMap) []string { 254 cwd, err := os.Getwd() 255 if err != nil { 256 return nil 257 } 258 var binds []string 259 for k, dockerPath := range resolved.Imports { 260 if strings.HasPrefix(dockerPath, DockerModsDir) { 261 hostPath := filepath.Join(cwd, FunctionsDir, m.Imports[k]) 262 binds = append(binds, hostPath+":"+dockerPath+":ro,z") 263 } 264 } 265 for module, mapping := range resolved.Scopes { 266 for k, dockerPath := range mapping { 267 if strings.HasPrefix(dockerPath, DockerModsDir) { 268 hostPath := filepath.Join(cwd, FunctionsDir, m.Scopes[module][k]) 269 binds = append(binds, hostPath+":"+dockerPath+":ro,z") 270 } 271 } 272 } 273 return binds 274 } 275 276 func resolveHostPath(hostPath string, fsys afero.Fs) string { 277 // All local fs imports will be mounted to /home/deno/modules 278 if filepath.IsAbs(hostPath) { 279 return getModulePath(hostPath) 280 } 281 rel := filepath.Join(FunctionsDir, hostPath) 282 exists, err := afero.Exists(fsys, rel) 283 if err != nil { 284 logger := GetDebugLogger() 285 fmt.Fprintln(logger, err) 286 } 287 if !exists { 288 return hostPath 289 } 290 if strings.HasPrefix(rel, FunctionsDir) { 291 suffix := strings.TrimPrefix(rel, FunctionsDir) 292 return path.Join(DockerFuncDirPath, filepath.ToSlash(suffix)) 293 } 294 // Directory imports need to be suffixed with / 295 // Ref: https://deno.com/manual@v1.33.0/basics/import_maps 296 if strings.HasSuffix(hostPath, string(filepath.Separator)) { 297 rel += string(filepath.Separator) 298 } 299 return getModulePath(rel) 300 } 301 302 func getModulePath(hostPath string) string { 303 mod := path.Join(DockerModsDir, GetPathHash(hostPath)) 304 if strings.HasSuffix(hostPath, string(filepath.Separator)) { 305 mod += "/" 306 } else if ext := filepath.Ext(hostPath); len(ext) > 0 { 307 mod += ext 308 } 309 return mod 310 } 311 312 func GetPathHash(path string) string { 313 digest := sha256.Sum256([]byte(path)) 314 return hex.EncodeToString(digest[:]) 315 } 316 317 func AbsImportMapPath(importMapPath, slug string, fsys afero.Fs) (string, error) { 318 if importMapPath == "" { 319 if functionConfig, ok := Config.Functions[slug]; ok && functionConfig.ImportMap != "" { 320 importMapPath = functionConfig.ImportMap 321 if !filepath.IsAbs(importMapPath) { 322 importMapPath = filepath.Join(SupabaseDirPath, importMapPath) 323 } 324 } else if exists, err := afero.Exists(fsys, FallbackImportMapPath); exists { 325 importMapPath = FallbackImportMapPath 326 } else { 327 if err != nil { 328 logger := GetDebugLogger() 329 fmt.Fprintln(logger, err) 330 } 331 return importMapPath, nil 332 } 333 } 334 resolved, err := filepath.Abs(importMapPath) 335 if err != nil { 336 return "", err 337 } 338 if f, err := fsys.Stat(resolved); err != nil { 339 return "", errors.Errorf("Failed to read import map: %w", err) 340 } else if f.IsDir() { 341 return "", errors.New("Importing directory is unsupported: " + resolved) 342 } 343 return resolved, nil 344 } 345 346 func AbsTempImportMapPath(cwd, hostPath string) string { 347 name := GetPathHash(hostPath) + ".json" 348 return filepath.Join(cwd, ImportMapsDir, name) 349 } 350 351 func BindImportMap(hostImportMapPath, dockerImportMapPath string, fsys afero.Fs) ([]string, error) { 352 importMap, err := NewImportMap(hostImportMapPath, fsys) 353 if err != nil { 354 return nil, err 355 } 356 resolved := importMap.Resolve(fsys) 357 binds := importMap.BindModules(resolved) 358 if len(binds) > 0 { 359 cwd, err := os.Getwd() 360 if err != nil { 361 return nil, errors.Errorf("failed to get working directory: %w", err) 362 } 363 contents, err := json.MarshalIndent(resolved, "", " ") 364 if err != nil { 365 return nil, errors.Errorf("failed to encode json: %w", err) 366 } 367 // Rewrite import map to temporary host path 368 hostImportMapPath = AbsTempImportMapPath(cwd, hostImportMapPath) 369 if err := WriteFile(hostImportMapPath, contents, fsys); err != nil { 370 return nil, err 371 } 372 } 373 binds = append(binds, hostImportMapPath+":"+dockerImportMapPath+":ro,z") 374 return binds, nil 375 }