mirror of
https://github.com/go-gitea/gitea.git
synced 2026-04-05 07:51:17 +02:00
Replace webpack with Vite 8 as the frontend bundler. Frontend build is around 3-4 times faster than before. Will work on all platforms including riscv64 (via wasm). `iife.js` is a classic render-blocking script in `<head>` (handles web components/early DOM setup). `index.js` is loaded as a `type="module"` script in the footer. All other JS chunks are also module scripts (supported in all browsers since 2018). Entry filenames are content-hashed (e.g. `index.C6Z2MRVQ.js`) and resolved at runtime via the Vite manifest, eliminating the `?v=` cache busting (which was unreliable in some scenarios like vscode dev build). Replaces: https://github.com/go-gitea/gitea/pull/36896 Fixes: https://github.com/go-gitea/gitea/issues/17793 Signed-off-by: silverwind <me@silverwind.io> Signed-off-by: wxiaoguang <wxiaoguang@gmail.com> Co-authored-by: Claude (Opus 4.6) <noreply@anthropic.com> Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
157 lines
4.4 KiB
Go
157 lines
4.4 KiB
Go
// Copyright 2026 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package public
|
|
|
|
import (
|
|
"io"
|
|
"path"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"code.gitea.io/gitea/modules/json"
|
|
"code.gitea.io/gitea/modules/log"
|
|
"code.gitea.io/gitea/modules/setting"
|
|
)
|
|
|
|
type manifestEntry struct {
|
|
File string `json:"file"`
|
|
Name string `json:"name"`
|
|
IsEntry bool `json:"isEntry"`
|
|
CSS []string `json:"css"`
|
|
}
|
|
|
|
type manifestDataStruct struct {
|
|
paths map[string]string // unhashed path -> hashed path
|
|
names map[string]string // hashed path -> 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) (map[string]string, map[string]string) {
|
|
var manifest map[string]manifestEntry
|
|
if err := json.Unmarshal(data, &manifest); err != nil {
|
|
log.Error("Failed to parse frontend manifest: %v", err)
|
|
return nil, nil
|
|
}
|
|
|
|
paths := make(map[string]string)
|
|
names := make(map[string]string)
|
|
for _, entry := range manifest {
|
|
if !entry.IsEntry || entry.Name == "" {
|
|
continue
|
|
}
|
|
// Build unhashed key from file path: "js/index.js", "css/theme-gitea-dark.css"
|
|
dir := path.Dir(entry.File)
|
|
ext := path.Ext(entry.File)
|
|
key := dir + "/" + entry.Name + ext
|
|
paths[key] = entry.File
|
|
names[entry.File] = entry.Name
|
|
// Map associated CSS files, e.g. "css/index.css" -> "css/index.B3zrQPqD.css"
|
|
for _, css := range entry.CSS {
|
|
cssKey := path.Dir(css) + "/" + entry.Name + path.Ext(css)
|
|
paths[cssKey] = css
|
|
names[css] = entry.Name
|
|
}
|
|
}
|
|
return paths, 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 {
|
|
paths, names := parseManifest(manifestContent)
|
|
data := &manifestDataStruct{
|
|
paths: paths,
|
|
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
|
|
}
|
|
|
|
// getHashedPath resolves an unhashed asset path (origin path) to its content-hashed path from the frontend manifest.
|
|
// Example: getHashedPath("js/index.js") returns "js/index.C6Z2MRVQ.js"
|
|
// Falls back to returning the input path unchanged if the manifest is unavailable.
|
|
func getHashedPath(originPath string) string {
|
|
data := getManifestData()
|
|
if p, ok := data.paths[originPath]; ok {
|
|
return p
|
|
}
|
|
return originPath
|
|
}
|
|
|
|
// AssetURI returns the URI for a frontend asset.
|
|
// It may return a relative path or a full URL depending on the StaticURLPrefix setting.
|
|
// In Vite dev mode, known entry points are mapped to their source paths
|
|
// so the reverse proxy serves them from the Vite dev server.
|
|
// In production, it resolves the content-hashed path from the manifest.
|
|
func AssetURI(originPath string) string {
|
|
if src := viteDevSourceURL(originPath); src != "" {
|
|
return src
|
|
}
|
|
return setting.StaticURLPrefix + "/assets/" + getHashedPath(originPath)
|
|
}
|
|
|
|
// AssetNameFromHashedPath returns the asset entry name for a given hashed asset path.
|
|
// Example: returns "theme-gitea-dark" for "css/theme-gitea-dark.CyAaQnn5.css".
|
|
// Returns empty string if the path is not found in the manifest.
|
|
func AssetNameFromHashedPath(hashedPath string) string {
|
|
return getManifestData().names[hashedPath]
|
|
}
|