mirror of
https://github.com/hanzoai/forge.git
synced 2026-10-30 15:00:39 +00:00
Source files keep their SPDX-License-Identifier and Hanzo copyright line; the Gitea, Gogs and LFS Test Server copyrights they used to repeat now live once in NOTICE, alongside LICENSE. The header lint accepts any copyright line above the SPDX line, so it no longer demands an upstream one. Co-authored-by: Hanzo Dev <[email protected]>
183 lines
5.2 KiB
Go
183 lines
5.2 KiB
Go
// Copyright 2026 Hanzo AI, Inc. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package public
|
|
|
|
import (
|
|
"html"
|
|
"html/template"
|
|
"io"
|
|
"path"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/hanzoai/git/modules/json"
|
|
"github.com/hanzoai/git/modules/log"
|
|
"github.com/hanzoai/git/modules/setting"
|
|
)
|
|
|
|
// https://vite.dev/guide/backend-integration
|
|
type manifestEntry struct {
|
|
File string `json:"file"`
|
|
Name string `json:"name"`
|
|
CSS []string `json:"css"`
|
|
Imports []string `json:"imports"`
|
|
}
|
|
|
|
type manifestDataStruct struct {
|
|
entries map[string]*manifestEntry // source path -> entry
|
|
names map[string]string // content-hashed output file -> entry name
|
|
modTime int64
|
|
checkTime time.Time
|
|
}
|
|
|
|
var (
|
|
manifestData atomic.Pointer[manifestDataStruct]
|
|
manifestFS = sync.OnceValue(AssetFS)
|
|
)
|
|
|
|
const manifestPath = "assets/.vite/manifest.json"
|
|
|
|
func parseManifest(data []byte) (entries map[string]*manifestEntry, names map[string]string) {
|
|
if err := json.Unmarshal(data, &entries); err != nil {
|
|
log.Error("Failed to parse frontend manifest: %v", err)
|
|
return nil, nil
|
|
}
|
|
names = make(map[string]string, len(entries))
|
|
for _, entry := range entries {
|
|
names[entry.File] = entry.Name
|
|
}
|
|
return entries, names
|
|
}
|
|
|
|
func reloadManifest(existingData *manifestDataStruct) *manifestDataStruct {
|
|
now := time.Now()
|
|
data := existingData
|
|
if data != nil && now.Sub(data.checkTime) < time.Second {
|
|
// a single request triggers multiple calls to getHashedPath
|
|
// do not check the manifest file too frequently
|
|
return data
|
|
}
|
|
|
|
f, err := manifestFS().Open(manifestPath)
|
|
if err != nil {
|
|
log.Error("Failed to open frontend manifest: %v", err)
|
|
return data
|
|
}
|
|
defer f.Close()
|
|
|
|
fi, err := f.Stat()
|
|
if err != nil {
|
|
log.Error("Failed to stat frontend manifest: %v", err)
|
|
return data
|
|
}
|
|
|
|
needReload := data == nil || fi.ModTime().UnixNano() != data.modTime
|
|
if !needReload {
|
|
return data
|
|
}
|
|
manifestContent, err := io.ReadAll(f)
|
|
if err != nil {
|
|
log.Error("Failed to read frontend manifest: %v", err)
|
|
return data
|
|
}
|
|
return storeManifestFromBytes(manifestContent, fi.ModTime().UnixNano(), now)
|
|
}
|
|
|
|
func storeManifestFromBytes(manifestContent []byte, modTime int64, checkTime time.Time) *manifestDataStruct {
|
|
entries, names := parseManifest(manifestContent)
|
|
data := &manifestDataStruct{
|
|
entries: entries,
|
|
names: names,
|
|
modTime: modTime,
|
|
checkTime: checkTime,
|
|
}
|
|
manifestData.Store(data)
|
|
return data
|
|
}
|
|
|
|
func getManifestData() *manifestDataStruct {
|
|
data := manifestData.Load()
|
|
|
|
// In production the manifest is immutable (embedded in the binary).
|
|
// In dev mode, check if it changed on disk (for watch-frontend).
|
|
if data == nil || !setting.IsProd {
|
|
data = reloadManifest(data)
|
|
}
|
|
if data == nil {
|
|
data = &manifestDataStruct{}
|
|
}
|
|
return data
|
|
}
|
|
|
|
// devAssetURL returns a source file's Vite dev server URL, panicking in dev/testing if it's absent.
|
|
func devAssetURL(src string) string {
|
|
if url := viteDevSourceURL(src); url != "" {
|
|
return url
|
|
}
|
|
setting.PanicInDevOrTesting("Failed to locate source file for asset: %s", src)
|
|
return ""
|
|
}
|
|
|
|
// AssetURI resolves a frontend asset by its source path (the Vite manifest key, e.g.
|
|
// "web_src/js/index.ts"). Dev mode serves the source file; production resolves the hashed output.
|
|
func AssetURI(srcPath string) string {
|
|
if IsViteDevMode() {
|
|
return devAssetURL(srcPath)
|
|
}
|
|
if entry := getManifestData().entries[srcPath]; entry != nil {
|
|
return setting.StaticURLPrefix + "/assets/" + entry.File
|
|
}
|
|
// The only expected manifest miss is a user's custom theme CSS, served as a static asset
|
|
// under "/assets/css/". Anything else is a misconfigured or missing entry.
|
|
if path.Ext(srcPath) == ".css" {
|
|
return setting.StaticURLPrefix + "/assets/css/" + path.Base(srcPath)
|
|
}
|
|
log.Error("asset not found in frontend manifest: %s", srcPath)
|
|
return setting.StaticURLPrefix + "/assets/" + path.Base(srcPath)
|
|
}
|
|
|
|
// AssetCSSLinks renders the <link> tags for a JS entry's stylesheets: the entry's CSS plus the CSS
|
|
// of every statically-imported chunk. Dev links devStylesheetSrc and lets the JS module inject the rest.
|
|
func AssetCSSLinks(jsEntrySrc, devStylesheetSrc string) template.HTML {
|
|
var b strings.Builder
|
|
for _, href := range entryStyleURLs(jsEntrySrc, devStylesheetSrc) {
|
|
b.WriteString(`<link rel="stylesheet" href="` + html.EscapeString(href) + `">`)
|
|
}
|
|
return template.HTML(b.String())
|
|
}
|
|
|
|
func entryStyleURLs(jsEntrySrc, devStylesheetSrc string) []string {
|
|
if IsViteDevMode() {
|
|
return []string{devAssetURL(devStylesheetSrc)}
|
|
}
|
|
entries := getManifestData().entries
|
|
var urls []string
|
|
seen := make(map[string]bool)
|
|
var walk func(key string)
|
|
walk = func(key string) {
|
|
entry := entries[key]
|
|
if entry == nil || seen[key] {
|
|
return
|
|
}
|
|
seen[key] = true
|
|
for _, css := range entry.CSS {
|
|
urls = append(urls, setting.StaticURLPrefix+"/assets/"+css)
|
|
}
|
|
for _, imp := range entry.Imports {
|
|
walk(imp)
|
|
}
|
|
}
|
|
walk(jsEntrySrc)
|
|
return urls
|
|
}
|
|
|
|
// AssetNameFromHashedPath returns the asset entry name for a given hashed asset path.
|
|
// Example: returns "theme-hanzo-dark" for "css/theme-hanzo-dark.CyAaQnn5.css".
|
|
// Returns empty string if the path is not found in the manifest.
|
|
func AssetNameFromHashedPath(hashedPath string) string {
|
|
return getManifestData().names[hashedPath]
|
|
}
|