Files
AstroJS/resolver-go/internal/resolver/resolver.go
T

390 lines
8.7 KiB
Go

package resolver
import (
"bytes"
"compress/zlib"
"crypto/md5"
"encoding/base64"
"encoding/hex"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
"sync"
"github.com/tgckpg/resolver-go/internal/classmap"
"github.com/tgckpg/resolver-go/internal/closure"
)
type OutputMode string
const (
ModeJS OutputMode = "js"
ModeCSS OutputMode = "css"
)
type Result struct {
Mode OutputMode
Request []string
JSFiles []classmap.Resource
CSSFiles []classmap.Resource
Hash string
Content []byte
ContentType string
}
type Resolver struct {
Root string
Map *classmap.Map
externMu sync.RWMutex
externCache map[string]closure.SourceInput
}
func New(root string, m *classmap.Map) (*Resolver, error) {
root, err := filepath.Abs(root)
if err != nil {
return nil, err
}
return &Resolver{
Root: root,
Map: m,
externCache: make(map[string]closure.SourceInput),
}, nil
}
func (r *Resolver) ResolveRequest(code string, mode OutputMode, excludes []string) (Result, error) {
apis, err := DecodeRequest(code)
if err != nil {
return Result{}, err
}
var includeSyms []classmap.Symbol
var excludeSyms []classmap.Symbol
for _, api := range apis {
if api == "" {
continue
}
neg := strings.HasPrefix(api, "-")
api = strings.TrimPrefix(api, "-")
syms, err := r.ResolveSymbol(api)
if err != nil {
return Result{}, err
}
if neg {
excludeSyms = append(excludeSyms, syms...)
} else {
includeSyms = append(includeSyms, syms...)
}
}
for _, ex := range excludes {
syms, err := r.ResolveSymbol(ex)
if err != nil {
return Result{}, err
}
excludeSyms = append(excludeSyms, syms...)
}
jsFiles := r.resourcesFor(includeSyms)
jsExcludes := r.resourcesFor(excludeSyms)
jsFiles = subtractResources(jsFiles, jsExcludes)
res := Result{Mode: mode, Request: apis, JSFiles: jsFiles}
switch mode {
case ModeJS:
content, hash, err := r.MergeJS(jsFiles)
res.Hash, res.Content, res.ContentType = hash, content, "application/javascript"
return res, err
case ModeCSS:
cssIn := r.cssResources(jsFiles)
cssEx := r.cssResources(jsExcludes)
cssFiles := subtractResources(cssIn, cssEx)
content, hash, err := r.MergeCSS(cssFiles)
res.CSSFiles, res.Hash, res.Content, res.ContentType = cssFiles, hash, content, "text/css"
return res, err
default:
return Result{}, fmt.Errorf("invalid mode: %s", mode)
}
}
func (r *Resolver) ResolveSymbol(name string) ([]classmap.Symbol, error) {
seen := map[string]bool{}
var out []classmap.Symbol
if err := r.resolveImport(name, seen, &out); err != nil {
return nil, err
}
return out, nil
}
func (r *Resolver) resolveImport(name string, seen map[string]bool, out *[]classmap.Symbol) error {
if strings.HasSuffix(name, ".*") {
return r.resolveWildcard(name, seen, out)
}
return r.resolveOne(name, seen, out)
}
func (r *Resolver) resolveWildcard(name string, seen map[string]bool, out *[]classmap.Symbol) error {
prefix := strings.TrimSuffix(name, "*")
namespace := strings.TrimSuffix(prefix, ".")
var keys []string
for k, s := range r.Map.Symbols {
if strings.HasPrefix(k, prefix) &&
s.Kind == classmap.KindClass &&
k != namespace {
keys = append(keys, k)
}
}
if len(keys) == 0 {
return fmt.Errorf("namespace does not exist or contains no classes: %s", name)
}
sort.Strings(keys)
for _, k := range keys {
if err := r.resolveOne(k, seen, out); err != nil {
return err
}
}
return nil
}
func (r *Resolver) resolveOne(name string, seen map[string]bool, out *[]classmap.Symbol) error {
if seen[name] {
return nil
}
sym, ok := r.Map.Symbols[name]
if !ok {
return fmt.Errorf("no such class: %s", name)
}
seen[name] = true
imports := sym.Imports
// Old resolver uses parent imports when the found node is prop/method.
if sym.Kind != classmap.KindClass && sym.Parent != "" {
if p, ok := r.Map.Symbols[sym.Parent]; ok {
imports = p.Imports
}
}
for _, imp := range imports {
if err := r.resolveImport(imp, seen, out); err != nil {
return err
}
}
*out = append(*out, sym)
return nil
}
func (r *Resolver) resourcesFor(syms []classmap.Symbol) []classmap.Resource {
var out []classmap.Resource
for _, s := range syms {
res, ok := r.resourceFor(s)
if ok {
out = appendResource(out, res)
}
}
return out
}
func (r *Resolver) resourceFor(s classmap.Symbol) (classmap.Resource, bool) {
if s.Resource.Src != "" {
return s.Resource, true
}
for p := s.Parent; p != ""; {
ps, ok := r.Map.Symbols[p]
if !ok {
break
}
if ps.Resource.Src != "" {
return ps.Resource, true
}
p = ps.Parent
}
return classmap.Resource{}, false
}
func (r *Resolver) cssResources(jsFiles []classmap.Resource) []classmap.Resource {
var out []classmap.Resource
for _, res := range jsFiles {
if res.CSSHash != "1" {
out = appendResource(out, res)
}
parts := strings.Split(res.Src, "/")
for i := 1; i < len(parts); i++ {
key := strings.Join(parts[:len(parts)-i], "/") + "/_this.js"
def, ok := r.Map.Files[key]
if ok && def.CSSHash != "1" {
out = appendResource(out, def)
}
}
}
sort.SliceStable(out, func(i, j int) bool { return out[i].Src < out[j].Src })
return out
}
func (r *Resolver) MergeJS(files []classmap.Resource) ([]byte, string, error) {
var b bytes.Buffer
head, _ := os.ReadFile(filepath.Join(r.Root, "_this.js"))
b.Write(head)
for _, f := range files {
pathName := strings.TrimSuffix(f.Src, ".js")
pathName = strings.ReplaceAll(pathName, "/", ".")
pathName = strings.ReplaceAll(pathName, "._this", "")
b.WriteString(";BotanJS.define(\"")
b.WriteString(pathName)
b.WriteString("\");")
src, err := os.ReadFile(filepath.Join(r.Root, filepath.FromSlash(f.Src)))
if err != nil {
return nil, "", err
}
b.Write(src)
}
wrapped := append([]byte("(function(){"), b.Bytes()...)
wrapped = append(wrapped, []byte("})();")...)
return wrapped, hashList(files, "js"), nil
}
func (r *Resolver) MergeCSS(files []classmap.Resource) ([]byte, string, error) {
var b bytes.Buffer
b.WriteString("/* @ */")
head, _ := os.ReadFile(filepath.Join(r.Root, "_this.css"))
b.Write(head)
for _, f := range files {
cssPath := strings.TrimSuffix(f.Src, ".js") + ".css"
src, err := os.ReadFile(filepath.Join(r.Root, filepath.FromSlash(cssPath)))
if err != nil {
if errors.Is(err, os.ErrNotExist) {
continue
}
return nil, "", err
}
b.Write(src)
}
return b.Bytes(), hashList(files, "css"), nil
}
func (r *Resolver) GetExterns(files []string) ([]closure.SourceInput, error) {
sources := make([]closure.SourceInput, 0, len(files))
for _, f := range files {
src, ok, err := r.getExtern(f)
if err != nil {
return nil, err
}
if !ok {
continue
}
sources = append(sources, src)
}
return sources, nil
}
func (r *Resolver) getExtern(f string) (closure.SourceInput, bool, error) {
// Fast path: read cache.
r.externMu.RLock()
src, ok := r.externCache[f]
r.externMu.RUnlock()
if ok {
return src, true, nil
}
// Slow path: read file.
b, err := os.ReadFile(filepath.Join(r.Root, filepath.FromSlash(f)))
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return closure.SourceInput{}, false, nil
}
return closure.SourceInput{}, false, err
}
src = closure.SourceInput{
Name: f,
Source: string(b),
}
// Store cache.
r.externMu.Lock()
r.externCache[f] = src
r.externMu.Unlock()
return src, true, nil
}
func DecodeRequest(code string) ([]string, error) {
sep := "/"
decoded := code
if raw, err := base64.StdEncoding.DecodeString(code); err == nil {
zr, err := zlib.NewReader(bytes.NewReader(raw))
if err == nil {
buf, readErr := io.ReadAll(zr)
_ = zr.Close()
if readErr != nil {
return nil, readErr
}
decoded = string(buf)
sep = ","
}
}
cleaner := regexp.MustCompile(`[^A-Za-z0-9.\*\-_/ ,]`)
decoded = cleaner.ReplaceAllString(decoded, "")
var fields []string
for _, p := range strings.Split(decoded, sep) {
p = strings.TrimSpace(p)
if p != "" {
fields = append(fields, p)
}
}
return fields, nil
}
func appendResource(out []classmap.Resource, res classmap.Resource) []classmap.Resource {
for _, x := range out {
if x.Src == res.Src {
return out
}
}
return append(out, res)
}
func subtractResources(in, ex []classmap.Resource) []classmap.Resource {
drop := map[string]bool{}
for _, x := range ex {
drop[x.Src] = true
}
var out []classmap.Resource
for _, x := range in {
if !drop[x.Src] {
out = append(out, x)
}
}
return out
}
func hashList(files []classmap.Resource, mode string) string {
var parts []string
for _, f := range files {
if mode == "css" {
parts = append(parts, f.CSSHash)
} else {
parts = append(parts, f.JSHash)
}
}
sum := md5.Sum([]byte(strings.Join(parts, "|")))
return hex.EncodeToString(sum[:]) + "." + mode
}