package sourcemaps import ( "fmt" "io" "os" "path/filepath" "regexp" "strconv" "strings" "sync" sm "github.com/go-sourcemap/sourcemap" ) var frameRE = regexp.MustCompile(`(.*?)(https?://[^ )]+|[^ )]+):(\d+):(\d+)(\)?\s*)$`) type Resolver struct { dir string mu sync.RWMutex maps map[string]*sm.Consumer } func New(dir string) (*Resolver, error) { if dir == "" { dir = "./sourcemaps" } if err := os.MkdirAll(dir, 0750); err != nil { return nil, err } return &Resolver{dir: dir, maps: make(map[string]*sm.Consumer)}, nil } func key(release, source string) string { return release + "\x00" + normalize(source) } func normalize(source string) string { source = strings.Split(source, "?")[0] if i := strings.Index(source, "/assets/"); i >= 0 { return source[i:] } return strings.TrimPrefix(source, "./") } func (r *Resolver) Upload(release, source string, body io.Reader) error { if strings.TrimSpace(release) == "" || strings.TrimSpace(source) == "" { return fmt.Errorf("release and source are required") } b, err := io.ReadAll(io.LimitReader(body, 50<<20)) if err != nil { return err } consumer, err := sm.Parse(source+".map", b) if err != nil { return fmt.Errorf("invalid source map: %w", err) } name := filepath.Join(r.dir, safe(release)+"-"+safe(normalize(source))+".map") if err := os.WriteFile(name, b, 0600); err != nil { return err } r.mu.Lock() r.maps[key(release, source)] = consumer r.mu.Unlock() return nil } func safe(s string) string { s = strings.ReplaceAll(s, "/", "_") s = strings.ReplaceAll(s, "\\", "_") return strings.Map(func(r rune) rune { if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || strings.ContainsRune("._-", r) { return r } return '_' }, s) } func (r *Resolver) ResolveStack(release, stack string) string { if release == "" || stack == "" { return stack } r.mu.RLock() defer r.mu.RUnlock() lines := strings.Split(stack, "\n") for i, line := range lines { m := frameRE.FindStringSubmatch(line) if m == nil { continue } genLine, _ := strconv.Atoi(m[3]) genCol, _ := strconv.Atoi(m[4]) consumer := r.maps[key(release, m[2])] if consumer == nil { continue } source, name, srcLine, srcCol, ok := consumer.Source(genLine, genCol) if !ok { continue } label := source if name != "" { label += " (" + name + ")" } lines[i] = fmt.Sprintf("%s%s:%d:%d%s [mapped from %s:%d:%d]", m[1], label, srcLine, srcCol, m[5], m[2], genLine, genCol) } return strings.Join(lines, "\n") }