支持 Minify AutoReversion 等
This commit is contained in:
parent
951a9a7397
commit
308def589d
14
go.mod
14
go.mod
@ -16,9 +16,10 @@ require (
|
|||||||
github.com/ssgo/httpclient v1.7.9
|
github.com/ssgo/httpclient v1.7.9
|
||||||
github.com/ssgo/log v1.7.11
|
github.com/ssgo/log v1.7.11
|
||||||
github.com/ssgo/redis v1.7.8
|
github.com/ssgo/redis v1.7.8
|
||||||
github.com/ssgo/s v1.7.26
|
github.com/ssgo/s v1.7.30
|
||||||
github.com/ssgo/standard v1.7.8
|
github.com/ssgo/standard v1.7.8
|
||||||
github.com/ssgo/u v1.7.24
|
github.com/ssgo/u v1.7.24
|
||||||
|
github.com/tdewolff/minify/v2 v2.24.11
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@ -32,15 +33,16 @@ require (
|
|||||||
github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect
|
github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect
|
||||||
github.com/gomodule/redigo v1.9.3 // indirect
|
github.com/gomodule/redigo v1.9.3 // indirect
|
||||||
github.com/google/pprof v0.0.0-20260302011040-a15ffb7f9dcc // indirect
|
github.com/google/pprof v0.0.0-20260302011040-a15ffb7f9dcc // indirect
|
||||||
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect
|
github.com/lufia/plan9stats v0.0.0-20260324052639-156f7da3f749 // indirect
|
||||||
github.com/obscuren/ecies v0.0.0-20150213224233-7c0f4a9b18d9 // indirect
|
github.com/obscuren/ecies v0.0.0-20150213224233-7c0f4a9b18d9 // indirect
|
||||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||||
github.com/robfig/cron/v3 v3.0.1 // indirect
|
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||||
github.com/shirou/gopsutil/v3 v3.24.5 // indirect
|
github.com/shirou/gopsutil/v3 v3.24.5 // indirect
|
||||||
github.com/shoenig/go-m1cpu v0.1.6 // indirect
|
github.com/shoenig/go-m1cpu v0.2.1 // indirect
|
||||||
github.com/ssgo/tool v0.4.29 // indirect
|
github.com/ssgo/tool v0.4.29 // indirect
|
||||||
github.com/tklauser/go-sysconf v0.3.15 // indirect
|
github.com/tdewolff/parse/v2 v2.8.11 // indirect
|
||||||
github.com/tklauser/numcpus v0.10.0 // indirect
|
github.com/tklauser/go-sysconf v0.3.16 // indirect
|
||||||
|
github.com/tklauser/numcpus v0.11.0 // indirect
|
||||||
github.com/ysmood/fetchup v0.2.3 // indirect
|
github.com/ysmood/fetchup v0.2.3 // indirect
|
||||||
github.com/ysmood/goob v0.4.0 // indirect
|
github.com/ysmood/goob v0.4.0 // indirect
|
||||||
github.com/ysmood/got v0.40.0 // indirect
|
github.com/ysmood/got v0.40.0 // indirect
|
||||||
@ -53,3 +55,5 @@ require (
|
|||||||
golang.org/x/text v0.35.0 // indirect
|
golang.org/x/text v0.35.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
|
replace github.com/ssgo/s => ../../ssgo/s
|
||||||
|
|||||||
205
service.go
205
service.go
@ -6,7 +6,9 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math"
|
||||||
"os"
|
"os"
|
||||||
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
"regexp"
|
"regexp"
|
||||||
@ -77,6 +79,9 @@ type Config struct {
|
|||||||
LimiterRedis string
|
LimiterRedis string
|
||||||
HotLoad int
|
HotLoad int
|
||||||
TplSafePaths []string
|
TplSafePaths []string
|
||||||
|
AutoReversion bool
|
||||||
|
AutoMinify bool
|
||||||
|
AutoTranslate bool
|
||||||
|
|
||||||
Proxy map[string]string
|
Proxy map[string]string
|
||||||
Rewrite map[string]string
|
Rewrite map[string]string
|
||||||
@ -89,6 +94,19 @@ var onStop goja.Callable
|
|||||||
var limiters = map[string]*s.Limiter{}
|
var limiters = map[string]*s.Limiter{}
|
||||||
var configed = false
|
var configed = false
|
||||||
|
|
||||||
|
var strTplRegexp = regexp.MustCompile(`%(\w+)%`)
|
||||||
|
|
||||||
|
func MakeByVar(text string, vars map[string]any) string {
|
||||||
|
text = strTplRegexp.ReplaceAllStringFunc(text, func(match string) string {
|
||||||
|
key := match[1 : len(match)-1]
|
||||||
|
if value, ok := vars[key]; ok {
|
||||||
|
return u.String(value)
|
||||||
|
}
|
||||||
|
return match
|
||||||
|
})
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
|
||||||
func initConfig(opt *gojs.Obj, logger *log.Logger, vm *goja.Runtime) {
|
func initConfig(opt *gojs.Obj, logger *log.Logger, vm *goja.Runtime) {
|
||||||
configed = true
|
configed = true
|
||||||
s.InitConfig()
|
s.InitConfig()
|
||||||
@ -96,7 +114,7 @@ func initConfig(opt *gojs.Obj, logger *log.Logger, vm *goja.Runtime) {
|
|||||||
s.SetWorkPath(u.String(startPath))
|
s.SetWorkPath(u.String(startPath))
|
||||||
}
|
}
|
||||||
// 处理配置
|
// 处理配置
|
||||||
serviceConfig = Config{"Session", "Device", "Client", "id", "name", "", 3600, "auth failed", "verify failed", "too many requests", nil, "", 0, []string{}, map[string]string{}, map[string]string{}, map[string]string{}}
|
serviceConfig = Config{"Session", "Device", "Client", "id", "name", "", 3600, "auth failed", "verify failed", "too many requests", nil, "", 0, []string{}, false, false, false, map[string]string{}, map[string]string{}, map[string]string{}}
|
||||||
if errs := config.LoadConfig("service", &serviceConfig); len(errs) > 0 {
|
if errs := config.LoadConfig("service", &serviceConfig); len(errs) > 0 {
|
||||||
// panic(vm.NewGoError(errs[0]))
|
// panic(vm.NewGoError(errs[0]))
|
||||||
vm.SetData("_lastError", errs[0])
|
vm.SetData("_lastError", errs[0])
|
||||||
@ -104,7 +122,6 @@ func initConfig(opt *gojs.Obj, logger *log.Logger, vm *goja.Runtime) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
config.LoadConfig("service", &discover.Config)
|
config.LoadConfig("service", &discover.Config)
|
||||||
// var auth goja.Callable
|
|
||||||
if opt != nil {
|
if opt != nil {
|
||||||
u.Convert(opt.O.Export(), &serviceConfig)
|
u.Convert(opt.O.Export(), &serviceConfig)
|
||||||
u.Convert(opt.O.Export(), &s.Config)
|
u.Convert(opt.O.Export(), &s.Config)
|
||||||
@ -247,8 +264,13 @@ func initConfig(opt *gojs.Obj, logger *log.Logger, vm *goja.Runtime) {
|
|||||||
}
|
}
|
||||||
}, nil, nil)
|
}, nil, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 处理多语言
|
||||||
|
// LoadLanguage("language.yml")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var resourceRegex = regexp.MustCompile(`(?i)(src|href|data|\w+)(\s*[=:]\s*)(["']?)([^"'\s>]+\.(js|css|json|svg|\w+))([^"'\s>]*)(["']?)`)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
s.Config.KeepKeyCase = true
|
s.Config.KeepKeyCase = true
|
||||||
s.DontStartLogAuto = true
|
s.DontStartLogAuto = true
|
||||||
@ -283,7 +305,8 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 处理Watch
|
// 处理Watch
|
||||||
if inWatch := vm.GetData("inWatch"); inWatch != nil && inWatch.(bool) {
|
inWatch := u.Bool(vm.GetData("inWatch"))
|
||||||
|
if inWatch {
|
||||||
onWatchConn := map[string]*websocket.Conn{}
|
onWatchConn := map[string]*websocket.Conn{}
|
||||||
onWatchLock := sync.Mutex{}
|
onWatchLock := sync.Mutex{}
|
||||||
vm.SetData("onWatch", func(filename string) {
|
vm.SetData("onWatch", func(filename string) {
|
||||||
@ -308,7 +331,7 @@ func init() {
|
|||||||
s.SetOutFilter(func(in map[string]any, request *s.Request, response *s.Response, out any, logger *log.Logger) (newOut any, isOver bool) {
|
s.SetOutFilter(func(in map[string]any, request *s.Request, response *s.Response, out any, logger *log.Logger) (newOut any, isOver bool) {
|
||||||
contentType := response.Header().Get("Content-Type")
|
contentType := response.Header().Get("Content-Type")
|
||||||
outStr := u.String(out)
|
outStr := u.String(out)
|
||||||
if strings.HasPrefix(contentType, "text/html") || (contentType == "" && strings.Contains(outStr, "<html")) {
|
if strings.HasPrefix(contentType, "text/html") || (contentType == "" && strings.Contains(outStr[0:int(math.Min(float64(len(outStr)), 100))], "<html")) {
|
||||||
if strings.Contains(outStr, "let _watchWS = null") {
|
if strings.Contains(outStr, "let _watchWS = null") {
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
@ -355,6 +378,166 @@ func init() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if inWatch {
|
||||||
|
// watch模式下,不自动压缩,方便调试定位脚本
|
||||||
|
serviceConfig.AutoMinify = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理自动版本号和自动压缩(非watch模式)
|
||||||
|
if serviceConfig.AutoReversion || serviceConfig.AutoMinify || serviceConfig.AutoTranslate {
|
||||||
|
s.SetOutFilter(func(in map[string]any, request *s.Request, response *s.Response, out any, logger *log.Logger) (newOut any, isOver bool) {
|
||||||
|
contentType := response.Header().Get("Content-Type")
|
||||||
|
outStr := u.String(out)
|
||||||
|
if strings.HasPrefix(contentType, "text/html") || (contentType == "" && strings.Contains(outStr[0:int(math.Min(float64(len(outStr)), 100))], "<html")) {
|
||||||
|
outChanged := false
|
||||||
|
newOutStr := resourceRegex.ReplaceAllStringFunc(outStr, func(match string) string {
|
||||||
|
subs := resourceRegex.FindStringSubmatch(match)
|
||||||
|
attr := subs[1] // src/href...
|
||||||
|
eq := subs[2]
|
||||||
|
quote1 := subs[3]
|
||||||
|
webPath := subs[4] // /res/app.js
|
||||||
|
ext := "." + subs[5] // .js
|
||||||
|
suffix := subs[6] // ?v=123456
|
||||||
|
quote2 := subs[7]
|
||||||
|
if strings.Contains(webPath, "://") || strings.HasPrefix(webPath, "//") || strings.HasPrefix(webPath, "data:") {
|
||||||
|
return match
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasPrefix(webPath, "/") {
|
||||||
|
if strings.HasSuffix(request.URL.Path, "/") {
|
||||||
|
webPath = request.URL.Path + webPath
|
||||||
|
} else {
|
||||||
|
webPath = path.Join(path.Dir(request.URL.Path), webPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取磁盘物理路径
|
||||||
|
localPath := s.GetStaticPath(webPath, request.Host)
|
||||||
|
if localPath == "" {
|
||||||
|
return match
|
||||||
|
}
|
||||||
|
|
||||||
|
changed := false
|
||||||
|
|
||||||
|
// 自动查找翻译文件
|
||||||
|
if serviceConfig.AutoTranslate {
|
||||||
|
language := ""
|
||||||
|
if cookieLanguage, err := request.Cookie("language"); err == nil {
|
||||||
|
language = cookieLanguage.Value
|
||||||
|
} else if uaLanguage := request.Header.Get("Accept-Language"); uaLanguage != "" {
|
||||||
|
language = uaLanguage
|
||||||
|
} else {
|
||||||
|
language = "en-US"
|
||||||
|
}
|
||||||
|
translationPath := strings.TrimSuffix(localPath, ext) + "." + language + ext
|
||||||
|
if u.FileExists(translationPath) {
|
||||||
|
localPath = translationPath
|
||||||
|
webPath = strings.TrimSuffix(webPath, ext) + "." + language + ext
|
||||||
|
changed = true
|
||||||
|
} else {
|
||||||
|
language2 := strings.Split(language, "-")[0]
|
||||||
|
translationPath = strings.TrimSuffix(localPath, ext) + "." + language2 + ext
|
||||||
|
if u.FileExists(translationPath) {
|
||||||
|
localPath = translationPath
|
||||||
|
webPath = strings.TrimSuffix(webPath, ext) + "." + language2 + ext
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if serviceConfig.AutoMinify && !strings.Contains(webPath, ".min.") && !strings.Contains(webPath, "-min.") && (ext == ".js" || ext == ".css" || ext == ".svg" || ext == ".json") {
|
||||||
|
localMinPath := strings.TrimSuffix(localPath, ext) + ".min" + ext
|
||||||
|
if err := Minify(localPath, localMinPath); err == nil {
|
||||||
|
webPath = strings.TrimSuffix(webPath, ext) + ".min" + ext
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if serviceConfig.AutoReversion {
|
||||||
|
if srcStat := u.GetFileInfo(localPath); srcStat != nil {
|
||||||
|
ver := uint64(srcStat.ModTime.Unix())
|
||||||
|
verStr := string(u.HashInt(u.EncodeInt(ver)))
|
||||||
|
if suffix == "" {
|
||||||
|
suffix = fmt.Sprintf("?v=%s", verStr)
|
||||||
|
} else if suffix[0] == '?' {
|
||||||
|
suffix = fmt.Sprintf("?v=%s&%s", verStr, suffix[1:])
|
||||||
|
} else {
|
||||||
|
suffix = fmt.Sprintf("?v=%s%s", verStr, suffix)
|
||||||
|
}
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if changed {
|
||||||
|
// 拼接新路径,保留原有属性名
|
||||||
|
outChanged = true
|
||||||
|
return fmt.Sprintf(`%s%s%s%s%s%s`, attr, eq, quote1, webPath, suffix, quote2)
|
||||||
|
}
|
||||||
|
return match
|
||||||
|
})
|
||||||
|
|
||||||
|
if outChanged {
|
||||||
|
return newOutStr, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理html自动压缩(非watch模式)
|
||||||
|
if (inWatch && serviceConfig.AutoReversion) || serviceConfig.AutoMinify || serviceConfig.AutoTranslate {
|
||||||
|
s.SetStaticRewriter(func(filename string, request *s.Request, response *s.Response, logger *log.Logger) string {
|
||||||
|
changed := false
|
||||||
|
isHTML := strings.HasSuffix(filename, ".html") || strings.HasSuffix(filename, ".htm")
|
||||||
|
if (inWatch && serviceConfig.AutoReversion) && isHTML {
|
||||||
|
// 开发模式下自动处理应用版本时,移除If-Modified-Since头,禁用html缓存
|
||||||
|
request.Header.Del("If-Modified-Since")
|
||||||
|
}
|
||||||
|
if serviceConfig.AutoMinify || serviceConfig.AutoTranslate {
|
||||||
|
if isHTML && !strings.Contains(filename, ".min.") && !strings.Contains(filename, "-min.") {
|
||||||
|
ext := filepath.Ext(filename)
|
||||||
|
|
||||||
|
if serviceConfig.AutoTranslate {
|
||||||
|
language := ""
|
||||||
|
if cookieLanguage, err := request.Cookie("language"); err == nil {
|
||||||
|
language = cookieLanguage.Value
|
||||||
|
} else if uaLanguage := request.Header.Get("Accept-Language"); uaLanguage != "" {
|
||||||
|
language = uaLanguage
|
||||||
|
} else {
|
||||||
|
language = "en-US"
|
||||||
|
}
|
||||||
|
translationPath := strings.TrimSuffix(filename, ext) + "." + language + ext
|
||||||
|
if u.FileExists(translationPath) {
|
||||||
|
filename = translationPath
|
||||||
|
changed = true
|
||||||
|
} else {
|
||||||
|
language2 := strings.Split(language, "-")[0]
|
||||||
|
translationPath = strings.TrimSuffix(filename, ext) + "." + language2 + ext
|
||||||
|
if u.FileExists(translationPath) {
|
||||||
|
filename = translationPath
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if serviceConfig.AutoMinify {
|
||||||
|
minFilename := strings.TrimSuffix(filename, ext) + ".min" + ext
|
||||||
|
if err := Minify(filename, minFilename); err == nil {
|
||||||
|
filename = minFilename
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if changed {
|
||||||
|
return filename
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// 启动服务
|
// 启动服务
|
||||||
server = s.AsyncStart()
|
server = s.AsyncStart()
|
||||||
waitChan = make(chan bool, 1)
|
waitChan = make(chan bool, 1)
|
||||||
@ -716,6 +899,19 @@ func init() {
|
|||||||
// }
|
// }
|
||||||
// return nil
|
// return nil
|
||||||
// },
|
// },
|
||||||
|
"minify": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
|
||||||
|
args := gojs.MakeArgs(&argsIn, vm).Check(2)
|
||||||
|
if err := Minify(args.Str(0), args.Str(1)); err == nil {
|
||||||
|
} else {
|
||||||
|
vm.SetData("_lastError", err.Error())
|
||||||
|
gojs.GetLogger(vm).Error(err.Error())
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
"getStaticPath": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
|
||||||
|
args := gojs.MakeArgs(&argsIn, vm).Check(2)
|
||||||
|
return vm.ToValue(s.GetStaticPath(args.Str(0), args.Str(1)))
|
||||||
|
},
|
||||||
"setTplReplaces": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
|
"setTplReplaces": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
|
||||||
args := gojs.MakeArgs(&argsIn, vm).Check(1)
|
args := gojs.MakeArgs(&argsIn, vm).Check(1)
|
||||||
replaces := args.Map(0)
|
replaces := args.Map(0)
|
||||||
@ -790,6 +986,7 @@ func init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(fnList) > 0 {
|
if len(fnList) > 0 {
|
||||||
tpl = tpl.Funcs(fnList)
|
tpl = tpl.Funcs(fnList)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,6 +25,8 @@ export default {
|
|||||||
// setTplFunc,
|
// setTplFunc,
|
||||||
setTplReplaces,
|
setTplReplaces,
|
||||||
tpl,
|
tpl,
|
||||||
|
minify,
|
||||||
|
getStaticPath,
|
||||||
}
|
}
|
||||||
|
|
||||||
function config(config?: Config): void { }
|
function config(config?: Config): void { }
|
||||||
@ -56,6 +58,8 @@ function id(size?: number): string { return '' }
|
|||||||
// function setTplFunc(fnList: Object): void { }
|
// function setTplFunc(fnList: Object): void { }
|
||||||
function setTplReplaces(replaces: Object): void { }
|
function setTplReplaces(replaces: Object): void { }
|
||||||
function tpl(file: string, data: Object, fnList?: Object): string { return '' }
|
function tpl(file: string, data: Object, fnList?: Object): string { return '' }
|
||||||
|
function minify(from: string, to: string): string { return '' }
|
||||||
|
function getStaticPath(requestPath: string, host?: string): string { return '' }
|
||||||
|
|
||||||
export interface Config {
|
export interface Config {
|
||||||
// github.com/ssgo/s 的配置参数
|
// github.com/ssgo/s 的配置参数
|
||||||
@ -122,6 +126,8 @@ export interface Config {
|
|||||||
limiters: Map<string, LimiterConfig> // 限流器配置,from 为数据来源,例如:ip、user、device、header.User-Agent、in.phone 等(in表示从请求参数中获取),time为时间间隔,单位ms,times为 时间间隔内允许访问的次数
|
limiters: Map<string, LimiterConfig> // 限流器配置,from 为数据来源,例如:ip、user、device、header.User-Agent、in.phone 等(in表示从请求参数中获取),time为时间间隔,单位ms,times为 时间间隔内允许访问的次数
|
||||||
hotLoad: number // 热加载配置,单位s,默认值:0,0表示不检测热加载
|
hotLoad: number // 热加载配置,单位s,默认值:0,0表示不检测热加载
|
||||||
tplSafePaths: string[] // 模板文件的安全路径,默认不开启安全检查
|
tplSafePaths: string[] // 模板文件的安全路径,默认不开启安全检查
|
||||||
|
autoReversion: boolean // 是否自动为html中引用的本地.js .css .json .svg 等文件添加版本号,默认不开启
|
||||||
|
autoMinify: boolean // 是否自动压缩html中引用的本地.js .css .json .svg 等文件,默认不开启
|
||||||
|
|
||||||
// gateway 的配置参数
|
// gateway 的配置参数
|
||||||
proxy: Map<string, string> // 代理配置,key为[host][path],value为代理的目标应用或URL
|
proxy: Map<string, string> // 代理配置,key为[host][path],value为代理的目标应用或URL
|
||||||
|
|||||||
99
web.go
Normal file
99
web.go
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/ssgo/u"
|
||||||
|
"github.com/tdewolff/minify/v2"
|
||||||
|
"github.com/tdewolff/minify/v2/css"
|
||||||
|
"github.com/tdewolff/minify/v2/html"
|
||||||
|
"github.com/tdewolff/minify/v2/js"
|
||||||
|
"github.com/tdewolff/minify/v2/json"
|
||||||
|
"github.com/tdewolff/minify/v2/svg"
|
||||||
|
"github.com/tdewolff/minify/v2/xml"
|
||||||
|
)
|
||||||
|
|
||||||
|
var m *minify.M
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
m = minify.New()
|
||||||
|
m.AddFunc("text/html", html.Minify)
|
||||||
|
m.AddFunc("text/css", css.Minify)
|
||||||
|
m.AddFunc("image/svg+xml", svg.Minify)
|
||||||
|
m.AddFuncRegexp(regexp.MustCompile("^(application|text)/(x-)?(java|ecma)script$"), js.Minify)
|
||||||
|
m.AddFuncRegexp(regexp.MustCompile("[/+]json$"), json.Minify)
|
||||||
|
m.AddFuncRegexp(regexp.MustCompile("[/+]xml$"), xml.Minify)
|
||||||
|
}
|
||||||
|
|
||||||
|
var reJSBacktickContent = regexp.MustCompile("(?s)`([^`]*)`")
|
||||||
|
var reHTMLBetweenTags = regexp.MustCompile(`>\s+<`)
|
||||||
|
|
||||||
|
func Minify(from, to string) error {
|
||||||
|
srcStat := u.GetFileInfo(from)
|
||||||
|
if srcStat == nil {
|
||||||
|
return errors.New("file not found: " + from)
|
||||||
|
}
|
||||||
|
dstStat := u.GetFileInfo(to)
|
||||||
|
if dstStat != nil && !srcStat.ModTime.After(dstStat.ModTime) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpTo := filepath.Join(filepath.Dir(to), "."+filepath.Base(to))
|
||||||
|
dst, err := os.OpenFile(tmpTo, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer os.Remove(tmpTo)
|
||||||
|
defer dst.Close()
|
||||||
|
|
||||||
|
if syscall.Flock(int(dst.Fd()), syscall.LOCK_EX) == nil {
|
||||||
|
defer syscall.Flock(int(dst.Fd()), syscall.LOCK_UN)
|
||||||
|
}
|
||||||
|
|
||||||
|
dstStat = u.GetFileInfo(to)
|
||||||
|
if dstStat != nil && !srcStat.ModTime.After(dstStat.ModTime) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
input := u.ReadFileN(from)
|
||||||
|
processedInput := reJSBacktickContent.ReplaceAllStringFunc(input, func(s string) string {
|
||||||
|
return "`" + reHTMLBetweenTags.ReplaceAllString(strings.TrimSpace(s[1:len(s)-1]), "><") + "`"
|
||||||
|
})
|
||||||
|
|
||||||
|
minifiedOutput, err := m.String(getMimeType(from), processedInput)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err = dst.WriteString(minifiedOutput); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = dst.Close(); err == nil {
|
||||||
|
err = os.Rename(tmpTo, to)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func getMimeType(filename string) string {
|
||||||
|
switch filepath.Ext(filename) {
|
||||||
|
case ".html", ".htm":
|
||||||
|
return "text/html"
|
||||||
|
case ".css":
|
||||||
|
return "text/css"
|
||||||
|
case ".js":
|
||||||
|
return "application/javascript"
|
||||||
|
case ".json":
|
||||||
|
return "application/json"
|
||||||
|
case ".svg":
|
||||||
|
return "image/svg+xml"
|
||||||
|
case ".xml":
|
||||||
|
return "text/xml"
|
||||||
|
default:
|
||||||
|
return "text/plain"
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user