Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
d99e932
Migrate from webpack to Vite 8
silverwind Mar 13, 2026
89b16a1
Use content-hashed filenames with Vite manifest for cache busting
silverwind Mar 13, 2026
90374c1
Hash all asset filenames and remove AssetVersion
silverwind Mar 13, 2026
a3d579e
Merge branch 'main' into vite
silverwind Mar 13, 2026
dea0436
Rename template helper `AssetPath` to `GetAssetPath` and clean up
silverwind Mar 13, 2026
581bd1b
cleanup
silverwind Mar 13, 2026
a419e10
Fix webcomponents manifest entry not written
silverwind Mar 13, 2026
e701562
inline
silverwind Mar 13, 2026
2a198b0
Remove duplicate `webcomponents-blocking.ts`
silverwind Mar 13, 2026
6533de7
Address review comments on `vite.config.ts`
silverwind Mar 13, 2026
e9554c7
Add upstream issue link for ENABLE_SOURCEMAP comment
silverwind Mar 13, 2026
df3c17a
add ref
silverwind Mar 13, 2026
6215c5e
Simplify ENABLE_SOURCEMAP handling
silverwind Mar 13, 2026
d6b34b3
Restore ENABLE_SOURCEMAP=reduced mode with Vite plugin
silverwind Mar 13, 2026
17ab78e
Restore original ENABLE_SOURCEMAP variable names from webpack config
silverwind Mar 13, 2026
c160f2b
Merge branch 'main' into vite
silverwind Mar 13, 2026
5fb3d15
Update modules/public/manifest.go
silverwind Mar 13, 2026
c52a9da
Restore ENABLE_SOURCEMAP=reduced mode with Vite plugin
silverwind Mar 13, 2026
3fcea7d
Apply suggestion from @silverwind
silverwind Mar 13, 2026
4a83e14
Build index.js as blocking IIFE, load index-domready as deferred module
silverwind Mar 13, 2026
43df852
Use global jQuery, remove jquery-global plugin, simplify CSS handling
silverwind Mar 13, 2026
e13f106
Use AssetFS() for Vite manifest reading
silverwind Mar 13, 2026
745cc7d
Simplify manifest loading, remove htmx from IIFE
silverwind Mar 14, 2026
70568dd
Use Vite's Manifest type for manifest handling
silverwind Mar 14, 2026
e0b1ad8
Add jQuery to vitest setup for global $ in tests
silverwind Mar 14, 2026
14744ac
Update stale webpack references to vite
silverwind Mar 14, 2026
ef8811f
forbid jquery imports because of single global instance
silverwind Mar 14, 2026
fee04f3
Suppress plugin timing warnings for worker builds
silverwind Mar 14, 2026
93ce326
Extract shared rolldownOptions for main, IIFE and worker builds
silverwind Mar 14, 2026
ae24785
use function
silverwind Mar 14, 2026
b5d5e46
Remove reduced sourcemap mode, simplify to true/false
silverwind Mar 14, 2026
bc05ab5
fix comment
wxiaoguang Mar 15, 2026
467dde9
add jQuery check to devtest page
wxiaoguang Mar 15, 2026
6ae005d
Use atomic.Pointer for manifest state to fix data race in dev mode
silverwind Mar 15, 2026
f3a8a19
Move htmx to IIFE globals, forbid `htmx.org` imports
silverwind Mar 15, 2026
e66d7b4
eslint tweaks
silverwind Mar 15, 2026
f0cf85d
C O M M E N T
wxiaoguang Mar 15, 2026
be08926
Stub XPathEvaluator in vitest setup for htmx compatibility
silverwind Mar 15, 2026
1487b7a
fix parseManifest
wxiaoguang Mar 15, 2026
1efff94
Stub XPathEvaluator in vitest setup for htmx compatibility
silverwind Mar 15, 2026
412ba3c
Merge tiny mermaid parser chunks via `manualChunks`
silverwind Mar 15, 2026
303328b
fix lint
silverwind Mar 15, 2026
18070c4
Use `codeSplitting` instead of deprecated `manualChunks`
silverwind Mar 15, 2026
c098499
Merge mermaid diagram chunks into single `mermaid-core` chunk
silverwind Mar 15, 2026
9ff8789
Remove redundant `minSize: 0` from mermaid-core group
silverwind Mar 15, 2026
4c5b77c
Remove unnecessary priority from vue-runtime group
silverwind Mar 15, 2026
c6f6ea8
add UnencryptedHTTP2
wxiaoguang Mar 15, 2026
2040535
Merge mermaid chunks without making them static imports
silverwind Mar 15, 2026
592b67b
fmt
silverwind Mar 15, 2026
e768b5c
comment
silverwind Mar 15, 2026
89f8907
Remove shared chunk dependencies from swagger entry
silverwind Mar 15, 2026
1dacad8
Move relative-time to own
silverwind Mar 15, 2026
93e92fa
Clean up vite config
silverwind Mar 15, 2026
80a31ec
Add citation-js codeSplitting group, update comment
silverwind Mar 15, 2026
f9201be
Merge branch 'main' into vite
silverwind Mar 15, 2026
2101c2e
explicit minify
silverwind Mar 15, 2026
0648217
Restore process.env.NODE_ENV define for IIFE build, remove citation-j…
silverwind Mar 15, 2026
4746fdd
Merge remote-tracking branch 'origin/main' into vite
silverwind Mar 17, 2026
4797006
Remove stale fomantic.css import
silverwind Mar 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ cpu.out
/yarn-error.log
/npm-debug.log*
/.pnpm-store
/public/assets/.vite
/public/assets/js
/public/assets/css
/public/assets/fonts
Expand Down
30 changes: 15 additions & 15 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -120,10 +120,10 @@ LINUX_ARCHS ?= linux/amd64,linux/386,linux/arm-5,linux/arm-6,linux/arm64,linux/r
GO_TEST_PACKAGES ?= $(filter-out $(shell $(GO) list code.gitea.io/gitea/models/migrations/...) code.gitea.io/gitea/tests/integration/migration-test code.gitea.io/gitea/tests code.gitea.io/gitea/tests/integration,$(shell $(GO) list ./... | grep -v /vendor/))
MIGRATE_TEST_PACKAGES ?= $(shell $(GO) list code.gitea.io/gitea/models/migrations/...)

WEBPACK_SOURCES := $(shell find web_src/js web_src/css -type f)
WEBPACK_CONFIGS := webpack.config.ts tailwind.config.ts
WEBPACK_DEST := public/assets/js/index.js public/assets/css/index.css
WEBPACK_DEST_ENTRIES := public/assets/js public/assets/css public/assets/fonts
FRONTEND_SOURCES := $(shell find web_src/js web_src/css -type f)
FRONTEND_CONFIGS := vite.config.ts tailwind.config.ts
FRONTEND_DEST := public/assets/.vite/manifest.json
FRONTEND_DEST_ENTRIES := public/assets/js public/assets/css public/assets/fonts public/assets/.vite

BINDATA_DEST_WILDCARD := modules/migration/bindata.* modules/public/bindata.* modules/options/bindata.* modules/templates/bindata.*

Expand Down Expand Up @@ -199,7 +199,7 @@ git-check:

.PHONY: clean-all
clean-all: clean ## delete backend, frontend and integration files
rm -rf $(WEBPACK_DEST_ENTRIES) node_modules
rm -rf $(FRONTEND_DEST_ENTRIES) node_modules

.PHONY: clean
clean: ## delete backend and integration files
Expand Down Expand Up @@ -381,8 +381,8 @@ watch: ## watch everything and continuously rebuild

.PHONY: watch-frontend
watch-frontend: node_modules ## watch frontend files and continuously rebuild
@rm -rf $(WEBPACK_DEST_ENTRIES)
NODE_ENV=development $(NODE_VARS) pnpm exec webpack --watch --progress --disable-interpret
@rm -rf $(FRONTEND_DEST_ENTRIES)
NODE_ENV=development $(NODE_VARS) pnpm exec vite build --watch

.PHONY: watch-backend
watch-backend: ## watch backend files and continuously rebuild
Expand Down Expand Up @@ -645,7 +645,7 @@ install: $(wildcard *.go)
build: frontend backend ## build everything

.PHONY: frontend
frontend: $(WEBPACK_DEST) ## build frontend files
frontend: $(FRONTEND_DEST) ## build frontend files

.PHONY: backend
backend: generate-backend $(EXECUTABLE) ## build backend files
Expand Down Expand Up @@ -776,15 +776,15 @@ update-py: node_modules ## update py dependencies
uv sync
@touch .venv

.PHONY: webpack
webpack: $(WEBPACK_DEST) ## build webpack files
.PHONY: vite
vite: $(FRONTEND_DEST) ## build vite files

$(WEBPACK_DEST): $(WEBPACK_SOURCES) $(WEBPACK_CONFIGS) pnpm-lock.yaml
$(FRONTEND_DEST): $(FRONTEND_SOURCES) $(FRONTEND_CONFIGS) pnpm-lock.yaml
@$(MAKE) -s node_modules
@rm -rf $(WEBPACK_DEST_ENTRIES)
@echo "Running webpack..."
@BROWSERSLIST_IGNORE_OLD_DATA=true $(NODE_VARS) pnpm exec webpack --disable-interpret
@touch $(WEBPACK_DEST)
@rm -rf $(FRONTEND_DEST_ENTRIES)
@echo "Running vite build..."
@$(NODE_VARS) pnpm exec vite build
@touch $(FRONTEND_DEST)

.PHONY: svg
svg: node_modules ## build svg files
Expand Down
2 changes: 1 addition & 1 deletion eslint.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1007,6 +1007,6 @@ export default defineConfig([
},
{
files: ['web_src/**/*'],
languageOptions: {globals: {...globals.browser, ...globals.webpack}},
languageOptions: {globals: globals.browser},
},
]);
9 changes: 5 additions & 4 deletions modules/markup/external/openapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"io"

"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/public"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
)
Expand Down Expand Up @@ -61,19 +62,19 @@ func (p *openAPIRenderer) Render(ctx *markup.RenderContext, input io.Reader, out
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="%s/assets/css/swagger.css?v=%s">
<link rel="stylesheet" href="%s/assets/%s">
</head>
<body>
<div id="swagger-ui"><textarea class="swagger-spec-content" data-spec-filename="%s">%s</textarea></div>
<script src="%s/assets/js/swagger.js?v=%s"></script>
<script type="module" src="%s/assets/%s"></script>
</body>
</html>`,
setting.StaticURLPrefix,
setting.AssetVersion,
public.GetAssetPath("css/swagger.css"),
html.EscapeString(ctx.RenderOptions.RelativePath),
html.EscapeString(util.UnsafeBytesToString(content)),
setting.StaticURLPrefix,
setting.AssetVersion,
public.GetAssetPath("js/swagger.js"),
))
return err
}
7 changes: 4 additions & 3 deletions modules/markup/render.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (

"code.gitea.io/gitea/modules/htmlutil"
"code.gitea.io/gitea/modules/markup/internal"
"code.gitea.io/gitea/modules/public"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/typesniffer"
"code.gitea.io/gitea/modules/util"
Expand Down Expand Up @@ -237,10 +238,10 @@ func RenderWithRenderer(ctx *RenderContext, renderer Renderer, input io.Reader,
return renderIFrame(ctx, extOpts.ContentSandbox, output)
}
// else: this is a standalone page, fallthrough to the real rendering, and add extra JS/CSS
extraStyleHref := setting.AppSubURL + "/assets/css/external-render-iframe.css"
extraScriptSrc := setting.AppSubURL + "/assets/js/external-render-iframe.js"
extraStyleHref := setting.AppSubURL + "/assets/" + public.GetAssetPath("css/external-render-iframe.css")
extraScriptSrc := setting.AppSubURL + "/assets/" + public.GetAssetPath("js/external-render-iframe.js")
// "<script>" must go before "<link>", to make Golang's http.DetectContentType() can still recognize the content as "text/html"
extraHeadHTML = htmlutil.HTMLFormat(`<script src="%s"></script><link rel="stylesheet" href="%s">`, extraScriptSrc, extraStyleHref)
extraHeadHTML = htmlutil.HTMLFormat(`<script type="module" src="%s"></script><link rel="stylesheet" href="%s">`, extraScriptSrc, extraStyleHref)
}

ctx.usedByRender = true
Expand Down
119 changes: 119 additions & 0 deletions modules/public/manifest.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package public

import (
"os"
"path"
"path/filepath"
"sync"

"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
)

type viteManifestEntry struct {
File string `json:"file"`
Name string `json:"name"`
IsEntry bool `json:"isEntry"`
CSS []string `json:"css"`
}

var (
manifestMu sync.RWMutex
manifestPaths map[string]string
manifestModTime int64
)

func manifestDiskPath() string {
return filepath.Join(setting.StaticRootPath, "public", "assets", ".vite", "manifest.json")
}

func parseManifest(data []byte) map[string]string {
var manifest map[string]viteManifestEntry
if err := json.Unmarshal(data, &manifest); err != nil {
log.Error("Failed to parse Vite manifest: %v", err)
return nil
}

paths := 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
// 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
}
}
return paths
}

func getManifestPaths() map[string]string {
diskPath := manifestDiskPath()

manifestMu.RLock()
if manifestPaths != nil {
fi, statErr := os.Stat(diskPath)
if statErr != nil || fi.ModTime().UnixNano() == manifestModTime {
paths := manifestPaths
manifestMu.RUnlock()
return paths
}
}
manifestMu.RUnlock()

manifestMu.Lock()
defer manifestMu.Unlock()

// Double-check after acquiring write lock
fi, statErr := os.Stat(diskPath)
if manifestPaths != nil {
if statErr != nil || fi.ModTime().UnixNano() == manifestModTime {
return manifestPaths
}
}

// Read from disk if available, otherwise from AssetFS (bindata)
var data []byte
var err error
if statErr == nil {
data, err = os.ReadFile(diskPath)
} else {
data, err = AssetFS().ReadFile("assets", ".vite", "manifest.json")
}
if err != nil {
log.Error("Failed to read Vite manifest: %v", err)
manifestPaths = make(map[string]string)
return manifestPaths
}

paths := parseManifest(data)
if paths == nil {
paths = make(map[string]string)
}
manifestPaths = paths
if fi != nil {
manifestModTime = fi.ModTime().UnixNano()
}
return manifestPaths
}

// GetAssetPath resolves an unhashed asset path to its content-hashed path from the Vite manifest.
// Example: GetAssetPath("js/index.js") returns "js/index.C6Z2MRVQ.js"
// Falls back to returning the input path unchanged if the manifest is unavailable.
func GetAssetPath(name string) string {
paths := getManifestPaths()
if p, ok := paths[name]; ok {
return p
}
return name
}
78 changes: 78 additions & 0 deletions modules/public/manifest_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package public

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestParseManifest(t *testing.T) {
manifest := []byte(`{
"web_src/js/index.ts": {
"file": "js/index.C6Z2MRVQ.js",
"name": "index",
"src": "web_src/js/index.ts",
"isEntry": true,
"css": ["css/index.B3zrQPqD.css"]
},
"web_src/js/standalone/swagger.ts": {
"file": "js/swagger.SujiEmYM.js",
"name": "swagger",
"src": "web_src/js/standalone/swagger.ts",
"isEntry": true,
"css": ["css/swagger._-APWT_3.css"]
},
"web_src/css/themes/theme-gitea-dark.css": {
"file": "css/theme-gitea-dark.CyAaQnn5.css",
"name": "theme-gitea-dark",
"src": "web_src/css/themes/theme-gitea-dark.css",
"isEntry": true
},
"web_src/js/features/sharedworker.ts": {
"file": "js/sharedworker.Dug1twio.js",
"name": "sharedworker",
"src": "web_src/js/features/sharedworker.ts",
"isEntry": true
},
"_chunk.js": {
"file": "js/chunk.abc123.js",
"name": "chunk"
}
}`)

paths := parseManifest(manifest)

// JS entries
assert.Equal(t, "js/index.C6Z2MRVQ.js", paths["js/index.js"])
assert.Equal(t, "js/swagger.SujiEmYM.js", paths["js/swagger.js"])
assert.Equal(t, "js/sharedworker.Dug1twio.js", paths["js/sharedworker.js"])

// Associated CSS from JS entries
assert.Equal(t, "css/index.B3zrQPqD.css", paths["css/index.css"])
assert.Equal(t, "css/swagger._-APWT_3.css", paths["css/swagger.css"])

// CSS-only entries
assert.Equal(t, "css/theme-gitea-dark.CyAaQnn5.css", paths["css/theme-gitea-dark.css"])

// Non-entry chunks should not be included
assert.Empty(t, paths["js/chunk.js"])
}

func TestGetAssetPathFallback(t *testing.T) {
// When manifest is not loaded, GetAssetPath should return the input as-is
manifestMu.Lock()
old := manifestPaths
manifestPaths = make(map[string]string)
manifestMu.Unlock()
defer func() {
manifestMu.Lock()
manifestPaths = old
manifestMu.Unlock()
}()

assert.Equal(t, "js/index.js", GetAssetPath("js/index.js"))
assert.Equal(t, "css/theme-gitea-dark.css", GetAssetPath("css/theme-gitea-dark.css"))
}
5 changes: 0 additions & 5 deletions modules/setting/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,6 @@ var (
// It maps to ini:"LOCAL_ROOT_URL" in [server]
LocalURL string

// AssetVersion holds an opaque value that is used for cache-busting assets
AssetVersion string

// appTempPathInternal is the temporary path for the app, it is only an internal variable
// DO NOT use it directly, always use AppDataTempDir
appTempPathInternal string
Expand Down Expand Up @@ -317,8 +314,6 @@ func loadServerFrom(rootCfg ConfigProvider) {
}

AbsoluteAssetURL = MakeAbsoluteAssetURL(appURL, StaticURLPrefix)
AssetVersion = strings.ReplaceAll(AppVer, "+", "~") // make sure the version string is clear (no real escaping is needed)

manifestBytes := MakeManifestData(AppName, AppURL, AbsoluteAssetURL)
ManifestData = `application/json;base64,` + base64.StdEncoding.EncodeToString(manifestBytes)

Expand Down
5 changes: 2 additions & 3 deletions modules/templates/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/htmlutil"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/public"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/svg"
"code.gitea.io/gitea/modules/templates/eval"
Expand Down Expand Up @@ -92,9 +93,7 @@ func NewFuncMap() template.FuncMap {
"AppDomain": func() string { // documented in mail-templates.md
return setting.Domain
},
"AssetVersion": func() string {
return setting.AssetVersion
},
"GetAssetPath": public.GetAssetPath,
"ShowFooterTemplateLoadTime": func() bool {
return setting.Other.ShowFooterTemplateLoadTime
},
Expand Down
Loading
Loading