Files
AstroJS/botanres-go/internal/classmap/scan.go
T
2026-06-11 08:07:38 +08:00

211 lines
4.6 KiB
Go

package classmap
import (
"bufio"
"crypto/sha1"
"encoding/hex"
"fmt"
"io"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
)
var (
reNamespace = regexp.MustCompile(`__namespace\s*\(\s*['\"]([^'\"]+)['\"]\s*\)`)
reImport = regexp.MustCompile(`__import\s*\(\s*['\"]([^'\"]+)['\"]\s*\)`)
reInvoke = regexp.MustCompile(`ns\s*\[\s*NS_INVOKE\s*\]\s*\(\s*['\"]([^'\"]+)['\"]\s*\)`)
reExport = regexp.MustCompile(`ns\s*\[\s*NS_EXPORT\s*\]\s*\(\s*EX_([A-Z_]+[A-Z])\s*,\s*['\"]([^'\"]+)['\"]\s*,`)
)
type Export struct {
Type string
Name string
}
type Meta struct {
Namespace string
Imports []string
Exports []Export
}
func ParseFile(path string) (Meta, error) {
f, err := os.Open(path)
if err != nil {
return Meta{}, err
}
defer f.Close()
var m Meta
s := bufio.NewScanner(f)
// Some old JS files can have long one-line wrappers.
s.Buffer(make([]byte, 0, 64*1024), 8*1024*1024)
for s.Scan() {
line := s.Text()
if x := reNamespace.FindStringSubmatch(line); x != nil {
m.Namespace = x[1]
continue
}
if x := reImport.FindStringSubmatch(line); x != nil {
m.Imports = append(m.Imports, x[1])
continue
}
if x := reInvoke.FindStringSubmatch(line); x != nil {
m.Imports = append(m.Imports, m.Namespace+"."+x[1])
continue
}
if x := reExport.FindStringSubmatch(line); x != nil {
m.Exports = append(m.Exports, Export{Type: x[1], Name: x[2]})
continue
}
}
return m, s.Err()
}
func Build(root string) (*Map, error) {
root, err := filepath.Abs(root)
if err != nil {
return nil, err
}
m := &Map{Symbols: map[string]Symbol{}, Files: map[string]Resource{}}
head := filepath.Join(root, "_this.js")
err = filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
if path == filepath.Join(root, "externs") {
return filepath.SkipDir
}
return nil
}
if filepath.Ext(path) != ".js" || path == head {
return nil
}
meta, err := ParseFile(path)
if err != nil {
return err
}
if meta.Namespace == "" {
return fmt.Errorf("%s: missing __namespace", path)
}
rel, err := filepath.Rel(root, path)
if err != nil {
return err
}
rel = filepath.ToSlash(rel)
res := Resource{Src: rel, JSHash: fileHash(path), CSSHash: fileHash(strings.TrimSuffix(path, ".js") + ".css")}
m.Files[rel] = res
srcEvery := meta.Namespace != className(rel)
ensureParents(m, meta.Namespace)
if !srcEvery {
sym := m.Symbols[meta.Namespace]
sym.Kind = KindClass
sym.Resource = res
sym.Imports = appendUnique(sym.Imports, meta.Imports...)
m.Symbols[meta.Namespace] = sym
}
for _, ex := range meta.Exports {
kind := exportKind(ex.Type)
name := meta.Namespace + "." + ex.Name
ensureParents(m, name)
sym := m.Symbols[name]
sym.Kind = kind
if srcEvery {
sym.Resource = res
if kind == KindClass {
sym.Imports = appendUnique(sym.Imports, meta.Imports...)
}
}
m.Symbols[name] = sym
}
return nil
})
if err != nil {
return nil, err
}
return m, nil
}
func ensureParents(m *Map, name string) {
parts := strings.Split(name, ".")
for i := range parts {
full := strings.Join(parts[:i+1], ".")
if _, ok := m.Symbols[full]; !ok {
parent := ""
if i > 0 {
parent = strings.Join(parts[:i], ".")
}
m.Symbols[full] = Symbol{Name: full, Kind: KindClass, Parent: parent}
}
}
}
func className(rel string) string {
name := strings.TrimSuffix(filepath.ToSlash(rel), ".js")
name = strings.ReplaceAll(name, "/", ".")
name = strings.ReplaceAll(name, "._this", "")
name = strings.ReplaceAll(name, "..BotanJS.", "")
return name
}
func exportKind(t string) Kind {
switch t {
case "CLASS":
return KindClass
case "FUNC":
return KindMethod
default:
return KindProp
}
}
func fileHash(path string) string {
f, err := os.Open(path)
if err != nil {
return "1"
}
defer f.Close()
h := sha1.New()
_, _ = io.Copy(h, f)
return hex.EncodeToString(h.Sum(nil))
}
func appendUnique(dst []string, vals ...string) []string {
seen := map[string]bool{}
for _, v := range dst {
seen[v] = true
}
for _, v := range vals {
if v == "" || seen[v] {
continue
}
dst = append(dst, v)
seen[v] = true
}
return dst
}
func SortedSymbols(m *Map) []Symbol {
out := make([]Symbol, 0, len(m.Symbols))
for _, s := range m.Symbols {
out = append(out, s)
}
sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name })
return out
}
func SortedFiles(m *Map) []Resource {
out := make([]Resource, 0, len(m.Files))
for _, r := range m.Files {
out = append(out, r)
}
sort.Slice(out, func(i, j int) bool { return out[i].Src < out[j].Src })
return out
}