From d99e9324a45a0063ceecf0b71018b3e9b2738f16 Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 13 Mar 2026 16:55:18 +0100 Subject: [PATCH 001/102] Migrate from webpack to Vite 8 Replace webpack with Vite 8 (Rolldown) as the frontend bundler. Key changes: - Replace webpack.config.ts with vite.config.ts using Rolldown - Use native ES modules with import maps for cache busting - Build web components as a separate blocking IIFE bundle to prevent flash of unstyled content (same behavior as webpack's blocking script) - Update all dynamic imports from webpack chunk comments to standard dynamic import() syntax - Replace process.env.TEST with import.meta.env.MODE - Add vite/client types to tsconfig.json - Update Monaco error suppression regex for new chunk names - Rename WEBPACK_* Makefile variables to FRONTEND_* Co-Authored-By: Claude (Opus 4.6) --- Makefile | 30 +- eslint.config.ts | 4 +- modules/markup/render.go | 2 +- package.json | 18 +- pnpm-lock.yaml | 1752 +++++------------ templates/base/head_script.tmpl | 6 +- templates/devtest/devtest-footer.tmpl | 2 +- templates/repo/diff/box.tmpl | 6 +- templates/shared/combomarkdowneditor.tmpl | 2 +- templates/swagger/ui.tmpl | 2 +- tests/integration/markup_external_test.go | 2 +- tsconfig.json | 2 +- vite.config.ts | 208 ++ web_src/js/bootstrap.ts | 6 +- web_src/js/features/captcha.ts | 2 +- web_src/js/features/citation.ts | 8 +- web_src/js/features/code-frequency.ts | 2 +- web_src/js/features/codeeditor.ts | 2 +- web_src/js/features/colorpicker.ts | 4 +- .../js/features/comp/ComboMarkdownEditor.ts | 4 +- web_src/js/features/comp/Cropper.ts | 2 +- web_src/js/features/contributors.ts | 2 +- web_src/js/features/dropzone.ts | 4 +- web_src/js/features/heatmap.ts | 2 +- web_src/js/features/notification.ts | 4 +- web_src/js/features/recent-commits.ts | 2 +- web_src/js/features/repo-findfile.ts | 2 +- web_src/js/features/repo-issue-pull.ts | 2 +- web_src/js/features/stopwatch.ts | 4 +- web_src/js/features/tribute.ts | 2 +- web_src/js/globals.d.ts | 9 + web_src/js/index.ts | 13 +- web_src/js/markup/asciicast.ts | 4 +- web_src/js/markup/math.ts | 4 +- web_src/js/markup/mermaid.ts | 4 +- web_src/js/markup/refissue.ts | 2 +- web_src/js/modules/monaco.ts | 17 + web_src/js/modules/sortable.ts | 2 +- web_src/js/render/plugins/3d-viewer.ts | 2 +- web_src/js/render/plugins/pdf-viewer.ts | 2 +- web_src/js/standalone/devtest.ts | 1 + .../js/standalone/external-render-iframe.ts | 2 + web_src/js/standalone/swagger.ts | 1 + web_src/js/utils.test.ts | 2 +- web_src/js/utils/dom.test.ts | 2 +- web_src/js/utils/testhelper.ts | 2 +- web_src/js/vitest.setup.ts | 2 - .../webcomponents/webcomponents-blocking.ts | 4 + webpack.config.ts | 269 --- 49 files changed, 786 insertions(+), 1648 deletions(-) create mode 100644 vite.config.ts create mode 100644 web_src/js/modules/monaco.ts create mode 100644 web_src/js/webcomponents/webcomponents-blocking.ts delete mode 100644 webpack.config.ts diff --git a/Makefile b/Makefile index 4d1bd96ea51c3..29ee94cc5cf20 100644 --- a/Makefile +++ b/Makefile @@ -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/js/index.js public/assets/js/webcomponents.js public/assets/css/index.css +FRONTEND_DEST_ENTRIES := public/assets/js public/assets/css public/assets/fonts BINDATA_DEST_WILDCARD := modules/migration/bindata.* modules/public/bindata.* modules/options/bindata.* modules/templates/bindata.* @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/eslint.config.ts b/eslint.config.ts index 8ed0cf789a799..a917bb82126b7 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -372,7 +372,7 @@ export default defineConfig([ 'import-x/no-unresolved': [2, {commonjs: true, ignore: ['\\?.+$']}], 'import-x/no-unused-modules': [0], // incompatible with eslint 9 'import-x/no-useless-path-segments': [2, {commonjs: true}], - 'import-x/no-webpack-loader-syntax': [2], + 'import-x/no-webpack-loader-syntax': [0], 'import-x/order': [0], 'import-x/prefer-default-export': [0], 'import-x/unambiguous': [0], @@ -1007,6 +1007,6 @@ export default defineConfig([ }, { files: ['web_src/**/*'], - languageOptions: {globals: {...globals.browser, ...globals.webpack}}, + languageOptions: {globals: globals.browser}, }, ]); diff --git a/modules/markup/render.go b/modules/markup/render.go index 5785dc5ad543b..fb7b4cf37cc37 100644 --- a/modules/markup/render.go +++ b/modules/markup/render.go @@ -240,7 +240,7 @@ func RenderWithRenderer(ctx *RenderContext, renderer Renderer, input io.Reader, extraStyleHref := setting.AppSubURL + "/assets/css/external-render-iframe.css" extraScriptSrc := setting.AppSubURL + "/assets/js/external-render-iframe.js" // "`, extraScriptSrc, extraStyleHref) + extraHeadHTML = htmlutil.HTMLFormat(``, extraScriptSrc, extraStyleHref) } ctx.usedByRender = true diff --git a/package.json b/package.json index 11dc64ee6a34a..c5142d2d93d3a 100644 --- a/package.json +++ b/package.json @@ -18,8 +18,7 @@ "@primer/octicons": "19.22.0", "@resvg/resvg-wasm": "2.6.2", "@silverwind/vue3-calendar-heatmap": "2.1.1", - "@techknowlogick/license-checker-webpack-plugin": "0.3.0", - "add-asset-webpack-plugin": "3.1.1", + "@vitejs/plugin-vue": "6.0.5", "ansi_up": "6.0.6", "asciinema-player": "3.15.1", "chart.js": "4.5.1", @@ -29,25 +28,21 @@ "colord": "2.9.3", "compare-versions": "6.1.1", "cropperjs": "1.6.2", - "css-loader": "7.1.4", "dayjs": "1.11.19", "dropzone": "6.0.0-beta.2", "easymde": "2.20.0", - "esbuild-loader": "4.4.2", "htmx.org": "2.0.8", "idiomorph": "0.7.4", "jquery": "4.0.0", "js-yaml": "4.1.1", "katex": "0.16.37", "mermaid": "11.12.3", - "mini-css-extract-plugin": "2.10.0", "monaco-editor": "0.55.1", - "monaco-editor-webpack-plugin": "7.1.1", "online-3d-viewer": "0.18.0", "pdfobject": "2.3.1", "perfect-debounce": "2.1.0", "postcss": "8.5.8", - "postcss-loader": "8.2.1", + "rollup-plugin-license": "3.7.0", "sortablejs": "1.15.7", "swagger-ui-dist": "5.32.0", "tailwindcss": "3.4.17", @@ -57,12 +52,11 @@ "tributejs": "5.1.3", "uint8-to-base64": "0.2.1", "vanilla-colorful": "0.7.2", + "vite": "8.0.0", + "vite-string-plugin": "2.0.2", "vue": "3.5.29", "vue-bar-graph": "2.2.0", "vue-chartjs": "5.3.3", - "vue-loader": "17.4.2", - "webpack": "5.105.4", - "webpack-cli": "6.0.1", "wrap-ansi": "10.0.0" }, "devDependencies": { @@ -83,7 +77,6 @@ "@types/throttle-debounce": "5.0.2", "@types/toastify-js": "1.12.4", "@typescript-eslint/parser": "8.56.1", - "@vitejs/plugin-vue": "6.0.4", "@vitest/eslint-plugin": "1.6.9", "eslint": "9.39.2", "eslint-import-resolver-typescript": "4.4.4", @@ -114,8 +107,7 @@ "typescript": "5.9.3", "typescript-eslint": "8.56.1", "updates": "17.8.3", - "vite-string-plugin": "2.0.1", - "vitest": "4.0.18", + "vitest": "4.1.0", "vue-tsc": "3.2.5" }, "pnpm": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 553364a5bc1f3..69e8ff8d324d2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -62,12 +62,9 @@ importers: '@silverwind/vue3-calendar-heatmap': specifier: 2.1.1 version: 2.1.1(tippy.js@6.3.7)(vue@3.5.29(typescript@5.9.3)) - '@techknowlogick/license-checker-webpack-plugin': - specifier: 0.3.0 - version: 0.3.0(webpack@5.105.4) - add-asset-webpack-plugin: - specifier: 3.1.1 - version: 3.1.1(webpack@5.105.4) + '@vitejs/plugin-vue': + specifier: 6.0.5 + version: 6.0.5(vite@8.0.0(@types/node@25.3.5)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2))(vue@3.5.29(typescript@5.9.3)) ansi_up: specifier: 6.0.6 version: 6.0.6 @@ -95,9 +92,6 @@ importers: cropperjs: specifier: 1.6.2 version: 1.6.2 - css-loader: - specifier: 7.1.4 - version: 7.1.4(webpack@5.105.4) dayjs: specifier: 1.11.19 version: 1.11.19 @@ -107,9 +101,6 @@ importers: easymde: specifier: 2.20.0 version: 2.20.0 - esbuild-loader: - specifier: 4.4.2 - version: 4.4.2(webpack@5.105.4) htmx.org: specifier: 2.0.8 version: 2.0.8 @@ -128,15 +119,9 @@ importers: mermaid: specifier: 11.12.3 version: 11.12.3 - mini-css-extract-plugin: - specifier: 2.10.0 - version: 2.10.0(webpack@5.105.4) monaco-editor: specifier: 0.55.1 version: 0.55.1 - monaco-editor-webpack-plugin: - specifier: 7.1.1 - version: 7.1.1(monaco-editor@0.55.1)(webpack@5.105.4) online-3d-viewer: specifier: 0.18.0 version: 0.18.0 @@ -149,9 +134,9 @@ importers: postcss: specifier: 8.5.8 version: 8.5.8 - postcss-loader: - specifier: 8.2.1 - version: 8.2.1(postcss@8.5.8)(typescript@5.9.3)(webpack@5.105.4) + rollup-plugin-license: + specifier: 3.7.0 + version: 3.7.0(picomatch@4.0.3)(rollup@4.59.0) sortablejs: specifier: 1.15.7 version: 1.15.7 @@ -179,6 +164,12 @@ importers: vanilla-colorful: specifier: 0.7.2 version: 0.7.2 + vite: + specifier: 8.0.0 + version: 8.0.0(@types/node@25.3.5)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2) + vite-string-plugin: + specifier: 2.0.2 + version: 2.0.2(vite@8.0.0(@types/node@25.3.5)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2)) vue: specifier: 3.5.29 version: 3.5.29(typescript@5.9.3) @@ -188,15 +179,6 @@ importers: vue-chartjs: specifier: 5.3.3 version: 5.3.3(chart.js@4.5.1)(vue@3.5.29(typescript@5.9.3)) - vue-loader: - specifier: 17.4.2 - version: 17.4.2(vue@3.5.29(typescript@5.9.3))(webpack@5.105.4) - webpack: - specifier: 5.105.4 - version: 5.105.4(webpack-cli@6.0.1) - webpack-cli: - specifier: 6.0.1 - version: 6.0.1(webpack@5.105.4) wrap-ansi: specifier: 10.0.0 version: 10.0.0 @@ -252,12 +234,9 @@ importers: '@typescript-eslint/parser': specifier: 8.56.1 version: 8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - '@vitejs/plugin-vue': - specifier: 6.0.4 - version: 6.0.4(vite@7.3.1(@types/node@25.3.5)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2))(vue@3.5.29(typescript@5.9.3)) '@vitest/eslint-plugin': specifier: 1.6.9 - version: 1.6.9(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)(vitest@4.0.18(@types/node@25.3.5)(happy-dom@20.8.3)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2)) + version: 1.6.9(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)(vitest@4.1.0(@types/node@25.3.5)(happy-dom@20.8.3)(vite@8.0.0(@types/node@25.3.5)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2))) eslint: specifier: 9.39.2 version: 9.39.2(jiti@2.6.1) @@ -345,12 +324,9 @@ importers: updates: specifier: 17.8.3 version: 17.8.3 - vite-string-plugin: - specifier: 2.0.1 - version: 2.0.1(vite@7.3.1(@types/node@25.3.5)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2)) vitest: - specifier: 4.0.18 - version: 4.0.18(@types/node@25.3.5)(happy-dom@20.8.3)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2) + specifier: 4.1.0 + version: 4.1.0(@types/node@25.3.5)(happy-dom@20.8.3)(vite@8.0.0(@types/node@25.3.5)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2)) vue-tsc: specifier: 3.2.5 version: 3.2.5(typescript@5.9.3) @@ -500,10 +476,6 @@ packages: peerDependencies: postcss-selector-parser: ^7.1.1 - '@discoveryjs/json-ext@0.6.3': - resolution: {integrity: sha512-4B4OijXeVNOPZlYA2oEwWOTkzyltLao+xbotHQeqN++Rv27Y6s818+n2Qkp8q+Fxhn0t/5lA5X1Mxktud8eayQ==} - engines: {node: '>=14.17.0'} - '@emnapi/core@1.8.1': resolution: {integrity: sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==} @@ -513,162 +485,6 @@ packages: '@emnapi/wasi-threads@1.1.0': resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} - '@esbuild/aix-ppc64@0.27.3': - resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [aix] - - '@esbuild/android-arm64@0.27.3': - resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [android] - - '@esbuild/android-arm@0.27.3': - resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==} - engines: {node: '>=18'} - cpu: [arm] - os: [android] - - '@esbuild/android-x64@0.27.3': - resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [android] - - '@esbuild/darwin-arm64@0.27.3': - resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [darwin] - - '@esbuild/darwin-x64@0.27.3': - resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==} - engines: {node: '>=18'} - cpu: [x64] - os: [darwin] - - '@esbuild/freebsd-arm64@0.27.3': - resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==} - engines: {node: '>=18'} - cpu: [arm64] - os: [freebsd] - - '@esbuild/freebsd-x64@0.27.3': - resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==} - engines: {node: '>=18'} - cpu: [x64] - os: [freebsd] - - '@esbuild/linux-arm64@0.27.3': - resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [linux] - - '@esbuild/linux-arm@0.27.3': - resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==} - engines: {node: '>=18'} - cpu: [arm] - os: [linux] - - '@esbuild/linux-ia32@0.27.3': - resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==} - engines: {node: '>=18'} - cpu: [ia32] - os: [linux] - - '@esbuild/linux-loong64@0.27.3': - resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==} - engines: {node: '>=18'} - cpu: [loong64] - os: [linux] - - '@esbuild/linux-mips64el@0.27.3': - resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==} - engines: {node: '>=18'} - cpu: [mips64el] - os: [linux] - - '@esbuild/linux-ppc64@0.27.3': - resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [linux] - - '@esbuild/linux-riscv64@0.27.3': - resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==} - engines: {node: '>=18'} - cpu: [riscv64] - os: [linux] - - '@esbuild/linux-s390x@0.27.3': - resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==} - engines: {node: '>=18'} - cpu: [s390x] - os: [linux] - - '@esbuild/linux-x64@0.27.3': - resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==} - engines: {node: '>=18'} - cpu: [x64] - os: [linux] - - '@esbuild/netbsd-arm64@0.27.3': - resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==} - engines: {node: '>=18'} - cpu: [arm64] - os: [netbsd] - - '@esbuild/netbsd-x64@0.27.3': - resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==} - engines: {node: '>=18'} - cpu: [x64] - os: [netbsd] - - '@esbuild/openbsd-arm64@0.27.3': - resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openbsd] - - '@esbuild/openbsd-x64@0.27.3': - resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [openbsd] - - '@esbuild/openharmony-arm64@0.27.3': - resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openharmony] - - '@esbuild/sunos-x64@0.27.3': - resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==} - engines: {node: '>=18'} - cpu: [x64] - os: [sunos] - - '@esbuild/win32-arm64@0.27.3': - resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==} - engines: {node: '>=18'} - cpu: [arm64] - os: [win32] - - '@esbuild/win32-ia32@0.27.3': - resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==} - engines: {node: '>=18'} - cpu: [ia32] - os: [win32] - - '@esbuild/win32-x64@0.27.3': - resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==} - engines: {node: '>=18'} - cpu: [x64] - os: [win32] - '@eslint-community/eslint-plugin-eslint-comments@4.7.1': resolution: {integrity: sha512-Ql2nJFwA8wUGpILYGOQaT1glPsmvEwE0d+a+l7AALLzQvInqdbXJdx7aSu0DpUX9dB1wMVBMhm99/++S3MdEtQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -816,6 +632,9 @@ packages: '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} + '@napi-rs/wasm-runtime@1.1.1': + resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -887,6 +706,13 @@ packages: resolution: {integrity: sha512-3dsKlf4Ma7o+uxLIg5OI1Tgwfet2pE8WTbPjEGWvOe6CSjMtK0skJnnSVHaEVX4N4mYU81To0qDeZOPqjaUotg==} engines: {node: '>=12.4.0'} + '@oxc-project/runtime@0.115.0': + resolution: {integrity: sha512-Rg8Wlt5dCbXhQnsXPrkOjL1DTSvXLgb2R/KYfnf1/K+R0k6UMLEmbQXPM+kwrWqSmWA2t0B1EtHy2/3zikQpvQ==} + engines: {node: ^20.19.0 || >=22.12.0} + + '@oxc-project/types@0.115.0': + resolution: {integrity: sha512-4n91DKnebUS4yjUHl2g3/b2T+IUdCfmoZGhmwsovZCDaJSs+QkVAM+0AqqTxHSsHfeiMuueT75cZaZcT/m0pSw==} + '@pkgr/core@0.2.9': resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} @@ -906,9 +732,107 @@ packages: resolution: {integrity: sha512-FqALmHI8D4o6lk/LRWDnhw95z5eO+eAa6ORjVg09YRR7BkcM6oPHU9uyC0gtQG5vpFLvgpeU4+zEAz2H8APHNw==} engines: {node: '>= 10'} + '@rolldown/binding-android-arm64@1.0.0-rc.9': + resolution: {integrity: sha512-lcJL0bN5hpgJfSIz/8PIf02irmyL43P+j1pTCfbD1DbLkmGRuFIA4DD3B3ZOvGqG0XiVvRznbKtN0COQVaKUTg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.0.0-rc.9': + resolution: {integrity: sha512-J7Zk3kLYFsLtuH6U+F4pS2sYVzac0qkjcO5QxHS7OS7yZu2LRs+IXo+uvJ/mvpyUljDJ3LROZPoQfgBIpCMhdQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.0.0-rc.9': + resolution: {integrity: sha512-iwtmmghy8nhfRGeNAIltcNXzD0QMNaaA5U/NyZc1Ia4bxrzFByNMDoppoC+hl7cDiUq5/1CnFthpT9n+UtfFyg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.0.0-rc.9': + resolution: {integrity: sha512-DLFYI78SCiZr5VvdEplsVC2Vx53lnA4/Ga5C65iyldMVaErr86aiqCoNBLl92PXPfDtUYjUh+xFFor40ueNs4Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.9': + resolution: {integrity: sha512-CsjTmTwd0Hri6iTw/DRMK7kOZ7FwAkrO4h8YWKoX/kcj833e4coqo2wzIFywtch/8Eb5enQ/lwLM7w6JX1W5RQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.9': + resolution: {integrity: sha512-2x9O2JbSPxpxMDhP9Z74mahAStibTlrBMW0520+epJH5sac7/LwZW5Bmg/E6CXuEF53JJFW509uP+lSedaUNxg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.9': + resolution: {integrity: sha512-JA1QRW31ogheAIRhIg9tjMfsYbglXXYGNPLdPEYrwFxdbkQCAzvpSCSHCDWNl4hTtrol8WeboCSEpjdZK8qrCg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.9': + resolution: {integrity: sha512-aOKU9dJheda8Kj8Y3w9gnt9QFOO+qKPAl8SWd7JPHP+Cu0EuDAE5wokQubLzIDQWg2myXq2XhTpOVS07qqvT+w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.9': + resolution: {integrity: sha512-OalO94fqj7IWRn3VdXWty75jC5dk4C197AWEuMhIpvVv2lw9fiPhud0+bW2ctCxb3YoBZor71QHbY+9/WToadA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.9': + resolution: {integrity: sha512-cVEl1vZtBsBZna3YMjGXNvnYYrOJ7RzuWvZU0ffvJUexWkukMaDuGhUXn0rjnV0ptzGVkvc+vW9Yqy6h8YX4pg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.9': + resolution: {integrity: sha512-UzYnKCIIc4heAKgI4PZ3dfBGUZefGCJ1TPDuLHoCzgrMYPb5Rv6TLFuYtyM4rWyHM7hymNdsg5ik2C+UD9VDbA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.9': + resolution: {integrity: sha512-+6zoiF+RRyf5cdlFQP7nm58mq7+/2PFaY2DNQeD4B87N36JzfF/l9mdBkkmTvSYcYPE8tMh/o3cRlsx1ldLfog==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.9': + resolution: {integrity: sha512-rgFN6sA/dyebil3YTlL2evvi/M+ivhfnyxec7AccTpRPccno/rPoNlqybEZQBkcbZu8Hy+eqNJCqfBR8P7Pg8g==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.9': + resolution: {integrity: sha512-lHVNUG/8nlF1IQk1C0Ci574qKYyty2goMiPlRqkC5R+3LkXDkL5Dhx8ytbxq35m+pkHVIvIxviD+TWLdfeuadA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.9': + resolution: {integrity: sha512-G0oA4+w1iY5AGi5HcDTxWsoxF509hrFIPB2rduV5aDqS9FtDg1CAfa7V34qImbjfhIcA8C+RekocJZA96EarwQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + '@rolldown/pluginutils@1.0.0-rc.2': resolution: {integrity: sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw==} + '@rolldown/pluginutils@1.0.0-rc.9': + resolution: {integrity: sha512-w6oiRWgEBl04QkFZgmW+jnU1EC9b57Oihi2ot3HNWIQRqgHp5PnYDia5iZ5FF7rpa4EQdiqMDXjlqKGXBhsoXw==} + '@rollup/rollup-android-arm-eabi@4.59.0': resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==} cpu: [arm] @@ -1100,11 +1024,6 @@ packages: '@swc/helpers@0.2.14': resolution: {integrity: sha512-wpCQMhf5p5GhNg2MmGKXzUNwxe7zRiCsmqYsamez2beP7mKPCSiu+BjZcdN95yYSzO857kr0VfQewmGpS77nqA==} - '@techknowlogick/license-checker-webpack-plugin@0.3.0': - resolution: {integrity: sha512-gqht/3IzjYttWGwVO5L+oPiQaO0SrPzpZCy/XGEcwTY5fpKs959+YhOHMiltJkLEfae60tE6s2jeOsxF547/sA==} - peerDependencies: - webpack: ^4.4.0 || ^5.4.0 - '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} @@ -1216,9 +1135,6 @@ packages: '@types/dropzone@5.7.9': resolution: {integrity: sha512-c6IlUz+DeQ4gANzJKn8fdP5rO6UyDNOyWLjfPbDRUHCNsXaAVKQOpuOv6LWEyvaK7pLqmoIpvSIlvBgGsk1vGw==} - '@types/eslint-scope@3.7.7': - resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} - '@types/eslint@9.6.1': resolution: {integrity: sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==} @@ -1450,11 +1366,11 @@ packages: cpu: [x64] os: [win32] - '@vitejs/plugin-vue@6.0.4': - resolution: {integrity: sha512-uM5iXipgYIn13UUQCZNdWkYk+sysBeA97d5mHsAoAt1u/wpN3+zxOmsVJWosuzX+IMGRzeYUNytztrYznboIkQ==} + '@vitejs/plugin-vue@6.0.5': + resolution: {integrity: sha512-bL3AxKuQySfk1iGcBsQnoRVexTPJq0Z/ixFVM8OhVJAP6ZXXXLtM7NFKWhLl30Kg7uTBqIaPXbh+nuQCuBDedg==} engines: {node: ^20.19.0 || >=22.12.0} peerDependencies: - vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 vue: ^3.2.25 '@vitest/eslint-plugin@1.6.9': @@ -1470,34 +1386,34 @@ packages: vitest: optional: true - '@vitest/expect@4.0.18': - resolution: {integrity: sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==} + '@vitest/expect@4.1.0': + resolution: {integrity: sha512-EIxG7k4wlWweuCLG9Y5InKFwpMEOyrMb6ZJ1ihYu02LVj/bzUwn2VMU+13PinsjRW75XnITeFrQBMH5+dLvCDA==} - '@vitest/mocker@4.0.18': - resolution: {integrity: sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==} + '@vitest/mocker@4.1.0': + resolution: {integrity: sha512-evxREh+Hork43+Y4IOhTo+h5lGmVRyjqI739Rz4RlUPqwrkFFDF6EMvOOYjTx4E8Tl6gyCLRL8Mu7Ry12a13Tw==} peerDependencies: msw: ^2.4.9 - vite: ^6.0.0 || ^7.0.0-0 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0-0 peerDependenciesMeta: msw: optional: true vite: optional: true - '@vitest/pretty-format@4.0.18': - resolution: {integrity: sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==} + '@vitest/pretty-format@4.1.0': + resolution: {integrity: sha512-3RZLZlh88Ib0J7NQTRATfc/3ZPOnSUn2uDBUoGNn5T36+bALixmzphN26OUD3LRXWkJu4H0s5vvUeqBiw+kS0A==} - '@vitest/runner@4.0.18': - resolution: {integrity: sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==} + '@vitest/runner@4.1.0': + resolution: {integrity: sha512-Duvx2OzQ7d6OjchL+trw+aSrb9idh7pnNfxrklo14p3zmNL4qPCDeIJAK+eBKYjkIwG96Bc6vYuxhqDXQOWpoQ==} - '@vitest/snapshot@4.0.18': - resolution: {integrity: sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==} + '@vitest/snapshot@4.1.0': + resolution: {integrity: sha512-0Vy9euT1kgsnj1CHttwi9i9o+4rRLEaPRSOJ5gyv579GJkNpgJK+B4HSv/rAWixx2wdAFci1X4CEPjiu2bXIMg==} - '@vitest/spy@4.0.18': - resolution: {integrity: sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==} + '@vitest/spy@4.1.0': + resolution: {integrity: sha512-pz77k+PgNpyMDv2FV6qmk5ZVau6c3R8HC8v342T2xlFxQKTrSeYw9waIJG8KgV9fFwAtTu4ceRzMivPTH6wSxw==} - '@vitest/utils@4.0.18': - resolution: {integrity: sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==} + '@vitest/utils@4.1.0': + resolution: {integrity: sha512-XfPXT6a8TZY3dcGY8EdwsBulFCIw+BeeX0RZn2x/BtiY/75YGh8FeWGG8QISN/WhaqSrE2OrlDgtF8q5uhOTmw==} '@volar/language-core@2.4.28': resolution: {integrity: sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==} @@ -1540,88 +1456,6 @@ packages: '@vue/shared@3.5.29': resolution: {integrity: sha512-w7SR0A5zyRByL9XUkCfdLs7t9XOHUyJ67qPGQjOou3p6GvBeBW+AVjUUmlxtZ4PIYaRvE+1LmK44O4uajlZwcg==} - '@webassemblyjs/ast@1.14.1': - resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} - - '@webassemblyjs/floating-point-hex-parser@1.13.2': - resolution: {integrity: sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==} - - '@webassemblyjs/helper-api-error@1.13.2': - resolution: {integrity: sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==} - - '@webassemblyjs/helper-buffer@1.14.1': - resolution: {integrity: sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==} - - '@webassemblyjs/helper-numbers@1.13.2': - resolution: {integrity: sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==} - - '@webassemblyjs/helper-wasm-bytecode@1.13.2': - resolution: {integrity: sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==} - - '@webassemblyjs/helper-wasm-section@1.14.1': - resolution: {integrity: sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==} - - '@webassemblyjs/ieee754@1.13.2': - resolution: {integrity: sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==} - - '@webassemblyjs/leb128@1.13.2': - resolution: {integrity: sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==} - - '@webassemblyjs/utf8@1.13.2': - resolution: {integrity: sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==} - - '@webassemblyjs/wasm-edit@1.14.1': - resolution: {integrity: sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==} - - '@webassemblyjs/wasm-gen@1.14.1': - resolution: {integrity: sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==} - - '@webassemblyjs/wasm-opt@1.14.1': - resolution: {integrity: sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==} - - '@webassemblyjs/wasm-parser@1.14.1': - resolution: {integrity: sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==} - - '@webassemblyjs/wast-printer@1.14.1': - resolution: {integrity: sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==} - - '@webpack-cli/configtest@3.0.1': - resolution: {integrity: sha512-u8d0pJ5YFgneF/GuvEiDA61Tf1VDomHHYMjv/wc9XzYj7nopltpG96nXN5dJRstxZhcNpV1g+nT6CydO7pHbjA==} - engines: {node: '>=18.12.0'} - peerDependencies: - webpack: ^5.82.0 - webpack-cli: 6.x.x - - '@webpack-cli/info@3.0.1': - resolution: {integrity: sha512-coEmDzc2u/ffMvuW9aCjoRzNSPDl/XLuhPdlFRpT9tZHmJ/039az33CE7uH+8s0uL1j5ZNtfdv0HkfaKRBGJsQ==} - engines: {node: '>=18.12.0'} - peerDependencies: - webpack: ^5.82.0 - webpack-cli: 6.x.x - - '@webpack-cli/serve@3.0.1': - resolution: {integrity: sha512-sbgw03xQaCLiT6gcY/6u3qBDn01CWw/nbaXl3gTdTFuJJ75Gffv3E3DBpgvY2fkkrdS1fpjaXNOmJlnbtKauKg==} - engines: {node: '>=18.12.0'} - peerDependencies: - webpack: ^5.82.0 - webpack-cli: 6.x.x - webpack-dev-server: '*' - peerDependenciesMeta: - webpack-dev-server: - optional: true - - '@xtuc/ieee754@1.2.0': - resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} - - '@xtuc/long@4.2.2': - resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} - - acorn-import-phases@1.0.4: - resolution: {integrity: sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==} - engines: {node: '>=10.13.0'} - peerDependencies: - acorn: ^8.14.0 - acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -1632,28 +1466,6 @@ packages: engines: {node: '>=0.4.0'} hasBin: true - add-asset-webpack-plugin@3.1.1: - resolution: {integrity: sha512-0WexE8uFq2hkC/rc+zVY8Hf5cKj/UwuBa0GSDKfCiKh6rrw/e7PYDgdxz1syHXIthTRlgt5q2vLvBIWcWtAvIQ==} - engines: {node: '>=18'} - peerDependencies: - webpack: '>=5' - peerDependenciesMeta: - webpack: - optional: true - - ajv-formats@2.1.1: - resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} - peerDependencies: - ajv: ^8.0.0 - peerDependenciesMeta: - ajv: - optional: true - - ajv-keywords@5.1.0: - resolution: {integrity: sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==} - peerDependencies: - ajv: ^8.8.2 - ajv@6.14.0: resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==} @@ -1740,9 +1552,6 @@ packages: engines: {node: '>=6.0.0'} hasBin: true - big.js@5.2.2: - resolution: {integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==} - binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} @@ -1849,10 +1658,6 @@ packages: chroma-js@3.2.0: resolution: {integrity: sha512-os/OippSlX1RlWWr+QDPcGUZs0uoqr32urfxESG9U93lhUfbnlyckte84Q8P1UQY/qth983AS1JONKmLS4T0nw==} - chrome-trace-event@1.0.4: - resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==} - engines: {node: '>=6.0'} - ci-info@4.4.0: resolution: {integrity: sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==} engines: {node: '>=8'} @@ -1867,10 +1672,6 @@ packages: clippie@4.1.10: resolution: {integrity: sha512-zUjK2fLH8/wju2lks5mH0u8wSRYCOJoHfT1KQ61+aCT5O1ouONnSrnKQ3BTKvIYLUYJarbLZo4FLHyce/SLF2g==} - clone-deep@4.0.1: - resolution: {integrity: sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==} - engines: {node: '>=6'} - codemirror-spell-checker@1.1.2: resolution: {integrity: sha512-2Tl6n0v+GJRsC9K3MLCdLaMOmvWL0uukajNJseorZJsslaxZyZMgENocPU8R0DyoTAiKsyqiemSOZo7kjGV0LQ==} @@ -1887,17 +1688,10 @@ packages: colord@2.9.3: resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==} - colorette@2.0.20: - resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} - commander@11.1.0: resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} engines: {node: '>=16'} - commander@12.1.0: - resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} - engines: {node: '>=18'} - commander@14.0.3: resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} engines: {node: '>=20'} @@ -1921,6 +1715,9 @@ packages: resolution: {integrity: sha512-aRDkn3uyIlCFfk5NUA+VdwMmMsh8JGhc4hapfV4yxymHGQ3BVskMQfoXGpCo5IoBuQ9tS5iiVKhCpTcB4pW4qw==} engines: {node: '>= 12.0.0'} + commenting@1.1.0: + resolution: {integrity: sha512-YeNK4tavZwtH7jEgK1ZINXzLKm6DZdEMfsaaieOsCAN0S8vsY7UeuO3Q7d/M018EFgE+IeUAuBOKkFccBZsUZA==} + compare-versions@6.1.1: resolution: {integrity: sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==} @@ -1930,6 +1727,9 @@ packages: confbox@0.1.8: resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + core-js-compat@3.48.0: resolution: {integrity: sha512-OM4cAF3D6VtH/WkLtWvyNC56EZVXsZdU3iqaMG2B4WvYrlqU831pc4UtG5yp0sE9z8Y02wVN7PjW5Zf9Gt0f1Q==} @@ -1962,18 +1762,6 @@ packages: resolution: {integrity: sha512-8HFEBPKhOpJPEPu70wJJetjKta86Gw9+CCyCnB3sui2qQfOvRyqBy4IKLKKAwdMpWb2lHXWk9Wb4Z6AmaUT1Pg==} engines: {node: '>=12'} - css-loader@7.1.4: - resolution: {integrity: sha512-vv3J9tlOl04WjiMvHQI/9tmIrCxVrj6PFbHemBB1iihpeRbi/I4h033eoFIhwxBBqLhI0KYFS7yvynBFhIZfTw==} - engines: {node: '>= 18.12.0'} - peerDependencies: - '@rspack/core': 0.x || ^1.0.0 || ^2.0.0-0 - webpack: ^5.27.0 - peerDependenciesMeta: - '@rspack/core': - optional: true - webpack: - optional: true - css-select@5.2.2: resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==} @@ -2201,6 +1989,10 @@ packages: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} @@ -2255,14 +2047,6 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - emojis-list@3.0.0: - resolution: {integrity: sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==} - engines: {node: '>= 4'} - - enhanced-resolve@5.20.0: - resolution: {integrity: sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==} - engines: {node: '>=10.13.0'} - entities@4.5.0: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} @@ -2275,30 +2059,12 @@ packages: resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} engines: {node: '>=6'} - envinfo@7.21.0: - resolution: {integrity: sha512-Lw7I8Zp5YKHFCXL7+Dz95g4CcbMEpgvqZNNq3AmlT5XAV6CgAAk6gyAMqn2zjw08K9BHfcNuKrMiCPLByGafow==} - engines: {node: '>=4'} - hasBin: true - error-ex@1.3.4: resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} - es-module-lexer@1.7.0: - resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} - es-module-lexer@2.0.0: resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==} - esbuild-loader@4.4.2: - resolution: {integrity: sha512-8LdoT9sC7fzfvhxhsIAiWhzLJr9yT3ggmckXxsgvM07wgrRxhuT98XhLn3E7VczU5W5AFsPKv9DdWcZIubbWkQ==} - peerDependencies: - webpack: ^4.40.0 || ^5.0.0 - - esbuild@0.27.3: - resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} - engines: {node: '>=18'} - hasBin: true - escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -2503,10 +2269,6 @@ packages: resolution: {integrity: sha512-pWReu3fkohwyvztx/oQWWgld2iad25TfUdi6wvhhaDPIQjHU/pyvlKgXFw1kX31SQK2Nq9MH+vRDWB0ZLy8fYw==} engines: {node: '>=4.0.0'} - eslint-scope@5.1.1: - resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} - engines: {node: '>=8.0.0'} - eslint-scope@8.4.0: resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2553,10 +2315,6 @@ packages: resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} engines: {node: '>=4.0'} - estraverse@4.3.0: - resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} - engines: {node: '>=4.0'} - estraverse@5.3.0: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} @@ -2638,10 +2396,6 @@ packages: resolution: {integrity: sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==} engines: {node: '>=18'} - find-up@4.1.0: - resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} - engines: {node: '>=8'} - find-up@5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} @@ -2653,16 +2407,9 @@ packages: flat-cache@6.1.20: resolution: {integrity: sha512-AhHYqwvN62NVLp4lObVXGVluiABTHapoB57EyegZVmazN+hhGhLTn3uZbOofoTw4DSDvVCadzzyChXhOAvy8uQ==} - flat@5.0.2: - resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==} - hasBin: true - flatted@3.3.4: resolution: {integrity: sha512-3+mMldrTAPdta5kjX2G2J7iX4zxtnwpdA8Tr2ZSjkyPSanvbZAcy6flmtnXbEybHrDcU9641lxrMfFuUxVz9vA==} - fs.realpath@1.0.0: - resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} - fsevents@2.3.2: resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -2691,13 +2438,6 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} - glob-to-regexp@0.4.1: - resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} - - glob@7.2.3: - resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me - global-modules@2.0.0: resolution: {integrity: sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==} engines: {node: '>=6'} @@ -2725,9 +2465,6 @@ packages: globjoin@0.1.4: resolution: {integrity: sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==} - graceful-fs@4.2.11: - resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - hachure-fill@0.5.2: resolution: {integrity: sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==} @@ -2747,9 +2484,6 @@ packages: resolution: {integrity: sha512-CsNUt5x9LUdx6hnk/E2SZLsDyvfqANZSUq4+D3D8RzDJ2M+HDTIkF60ibS1vHaK55vzgiZw1bEPFG9yH7l33wA==} engines: {node: '>=12'} - hash-sum@2.0.0: - resolution: {integrity: sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg==} - hashery@1.5.0: resolution: {integrity: sha512-nhQ6ExaOIqti2FDWoEMWARUqIKyjr2VcZzXShrI+A3zpeiuPWzx6iPftt44LhP74E5sW36B75N6VHbvRtpvO6Q==} engines: {node: '>=20'} @@ -2771,12 +2505,6 @@ packages: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} - icss-utils@5.1.0: - resolution: {integrity: sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==} - engines: {node: ^10 || ^12 || >= 14} - peerDependencies: - postcss: ^8.1.0 - idiomorph@0.7.4: resolution: {integrity: sha512-uCdSpLo3uMfqOmrwXTpR1k/sq4sSmKC7l4o/LdJOEU+MMMq+wkevRqOQYn3lP7vfz9Mv+USBEqPvi0XhdL9ENw==} @@ -2795,11 +2523,6 @@ packages: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} - import-local@3.2.0: - resolution: {integrity: sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==} - engines: {node: '>=8'} - hasBin: true - import-meta-resolve@4.2.0: resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==} @@ -2811,13 +2534,6 @@ packages: resolution: {integrity: sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==} engines: {node: '>=12'} - inflight@1.0.6: - resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} - deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. - - inherits@2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - ini@1.3.8: resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} @@ -2832,10 +2548,6 @@ packages: resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} engines: {node: '>=12'} - interpret@3.1.1: - resolution: {integrity: sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==} - engines: {node: '>=10.13.0'} - is-alphabetical@2.0.1: resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} @@ -2885,10 +2597,6 @@ packages: resolution: {integrity: sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==} engines: {node: '>=12'} - is-plain-object@2.0.4: - resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==} - engines: {node: '>=0.10.0'} - is-plain-object@5.0.0: resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} engines: {node: '>=0.10.0'} @@ -2902,14 +2610,6 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - isobject@3.0.1: - resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} - engines: {node: '>=0.10.0'} - - jest-worker@27.5.1: - resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} - engines: {node: '>= 10.13.0'} - jiti@1.21.7: resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} hasBin: true @@ -2962,11 +2662,6 @@ packages: resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} hasBin: true - json5@2.2.3: - resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} - engines: {node: '>=6'} - hasBin: true - jsonc-parser@3.3.1: resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} @@ -3027,6 +2722,80 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + lilconfig@3.1.3: resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} engines: {node: '>=14'} @@ -3037,18 +2806,6 @@ packages: linkify-it@5.0.0: resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} - loader-runner@4.3.1: - resolution: {integrity: sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==} - engines: {node: '>=6.11.5'} - - loader-utils@2.0.4: - resolution: {integrity: sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==} - engines: {node: '>=8.9.0'} - - locate-path@5.0.0: - resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} - engines: {node: '>=8'} - locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} @@ -3128,9 +2885,6 @@ packages: resolution: {integrity: sha512-EDYo6VlmtnumlcBCbh1gLJ//9jvM/ndXHfVXIFrZVr6fGcwTUyCTFNTLCKuY3ffbK8L/+3Mzqnd58RojiZqHVw==} engines: {node: '>=20'} - merge-stream@2.0.0: - resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} - merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -3217,20 +2971,6 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} - mime-db@1.52.0: - resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} - engines: {node: '>= 0.6'} - - mime-types@2.1.35: - resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} - engines: {node: '>= 0.6'} - - mini-css-extract-plugin@2.10.0: - resolution: {integrity: sha512-540P2c5dYnJlyJxTaSloliZexv8rji6rY8FhQN+WF/82iHQfA23j/xtJx97L+mXOML27EqksSek/g4eK7jaL3g==} - engines: {node: '>= 12.13.0'} - peerDependencies: - webpack: ^5.0.0 - minimatch@10.2.4: resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==} engines: {node: 18 || 20 || >=22} @@ -3244,11 +2984,8 @@ packages: mlly@1.8.1: resolution: {integrity: sha512-SnL6sNutTwRWWR/vcmCYHSADjiEesp5TGQQ0pXyLhW5IoeibRlF/CbSLailbB3CNqJUk9cVJ9dUDnbD7GrcHBQ==} - monaco-editor-webpack-plugin@7.1.1: - resolution: {integrity: sha512-WxdbFHS3Wtz4V9hzhe/Xog5hQRSMxmDLkEEYZwqMDHgJlkZo00HVFZR0j5d0nKypjTUkkygH3dDSXERLG4757A==} - peerDependencies: - monaco-editor: '>= 0.31.0' - webpack: ^4.5.0 || 5.x + moment@2.30.1: + resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==} monaco-editor@0.55.1: resolution: {integrity: sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==} @@ -3281,9 +3018,6 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - neo-async@2.6.2: - resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} - node-fetch@2.6.13: resolution: {integrity: sha512-StxNAxh15zr77QvvkmveSQ8uCQ4+v5FkvNTj0OESmiHu+VRi/gXArXtkWMElOsOUNLtUEvI4yS+rdtOHZTwlQA==} engines: {node: 4.x || >=6.0.0} @@ -3328,9 +3062,6 @@ packages: obug@2.1.1: resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} - once@1.4.0: - resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} - online-3d-viewer@0.18.0: resolution: {integrity: sha512-y7ZlV/zkakNUyjqcXz6XecA7vXgLEUnaAey9tyx8o6/wcdV64RfjXAQOjGXGY2JOZoDi4Cg1ic9icSWMWAvRQA==} @@ -3338,29 +3069,21 @@ packages: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} - p-limit@2.3.0: - resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} - engines: {node: '>=6'} - p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} - p-locate@4.1.0: - resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} - engines: {node: '>=8'} - p-locate@5.0.0: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} - p-try@2.2.0: - resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} - engines: {node: '>=6'} - package-manager-detector@1.6.0: resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==} + package-name-regex@2.0.6: + resolution: {integrity: sha512-gFL35q7kbE/zBaPA3UKhp2vSzcPYx2ecbYuwv1ucE9Il6IIgBDweBlH8D68UFGZic2MkllKa2KHCfC1IQBQUYA==} + engines: {node: '>=12'} + parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -3382,10 +3105,6 @@ packages: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} - path-is-absolute@1.0.1: - resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} - engines: {node: '>=0.10.0'} - path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} @@ -3421,10 +3140,6 @@ packages: resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} engines: {node: '>= 6'} - pkg-dir@4.2.0: - resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} - engines: {node: '>=8'} - pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} @@ -3476,43 +3191,6 @@ packages: ts-node: optional: true - postcss-loader@8.2.1: - resolution: {integrity: sha512-k98jtRzthjj3f76MYTs9JTpRqV1RaaMhEU0Lpw9OTmQZQdppg4B30VZ74BojuBHt3F4KyubHJoXCMUeM8Bqeow==} - engines: {node: '>= 18.12.0'} - peerDependencies: - '@rspack/core': 0.x || ^1.0.0 || ^2.0.0-0 - postcss: ^7.0.0 || ^8.0.1 - webpack: ^5.0.0 - peerDependenciesMeta: - '@rspack/core': - optional: true - webpack: - optional: true - - postcss-modules-extract-imports@3.1.0: - resolution: {integrity: sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==} - engines: {node: ^10 || ^12 || >= 14} - peerDependencies: - postcss: ^8.1.0 - - postcss-modules-local-by-default@4.2.0: - resolution: {integrity: sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==} - engines: {node: ^10 || ^12 || >= 14} - peerDependencies: - postcss: ^8.1.0 - - postcss-modules-scope@3.2.1: - resolution: {integrity: sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==} - engines: {node: ^10 || ^12 || >= 14} - peerDependencies: - postcss: ^8.1.0 - - postcss-modules-values@4.0.0: - resolution: {integrity: sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==} - engines: {node: ^10 || ^12 || >= 14} - peerDependencies: - postcss: ^8.1.0 - postcss-nested@6.2.0: resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==} engines: {node: '>=12.0'} @@ -3581,10 +3259,6 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} - rechoir@0.8.0: - resolution: {integrity: sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==} - engines: {node: '>= 10.13.0'} - refa@0.12.1: resolution: {integrity: sha512-J8rn6v4DBb2nnFqkqwy6/NnTYMcgLA+sLr0iIO41qpv0n+ngb7ksag2tMRl0inb1bbO/esUwzW1vbJi7K0sI0g==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} @@ -3609,18 +3283,10 @@ packages: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} - resolve-cwd@3.0.0: - resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} - engines: {node: '>=8'} - resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} - resolve-from@5.0.0: - resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} - engines: {node: '>=8'} - resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} @@ -3636,6 +3302,17 @@ packages: robust-predicates@3.0.2: resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==} + rolldown@1.0.0-rc.9: + resolution: {integrity: sha512-9EbgWge7ZH+yqb4d2EnELAntgPTWbfL8ajiTW+SyhJEC4qhBbkCKbqFV4Ge4zmu5ziQuVbWxb/XwLZ+RIO7E8Q==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + rollup-plugin-license@3.7.0: + resolution: {integrity: sha512-RvvOIF+GH3fBR3wffgc/vmjQn6qOn72WjppWVDp/v+CLpT0BbcRBdSkPeeIOL6U5XccdYgSIMjUyXgxlKEEFcw==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.0.0 || ^2.0.0 || ^3.0.0 || ^4.0.0 + rollup@4.59.0: resolution: {integrity: sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -3658,10 +3335,6 @@ packages: resolution: {integrity: sha512-21IYA3Q5cQf089Z6tgaUTr7lDAyzoTPx5HRtbhsME8Udispad8dC/+sziTNugOEx54ilvatQ9YCzl4KQLPcRHA==} engines: {node: '>=11.0.0'} - schema-utils@4.3.3: - resolution: {integrity: sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==} - engines: {node: '>= 10.13.0'} - scslre@0.3.0: resolution: {integrity: sha512-3A6sD0WYP7+QrjbfNA2FN3FsOaGGFoekCVgTyypy53gPxhbkCIjtO6YWgdrfM+n/8sI8JeXZOIxsHjMTNxQ4nQ==} engines: {node: ^14.0.0 || >=16.0.0} @@ -3685,10 +3358,6 @@ packages: resolution: {integrity: sha512-OE4cvmJ1uSPrKorFIH9/w/Qwuvi/IMcGbv5RKgcJ/zjA/IohDLU6SVaxFN9FwajbP7nsX0dQqMDes1whk3y+yw==} engines: {node: '>=10'} - shallow-clone@3.0.1: - resolution: {integrity: sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==} - engines: {node: '>=8'} - shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -3728,9 +3397,6 @@ packages: sortablejs@1.15.7: resolution: {integrity: sha512-Kk8wLQPlS+yi1ZEf48a4+fzHa4yxjC30M/Sr2AnQu+f/MPwvvX9XjZ6OWejiz8crBsLwSq8GHqaxaET7u6ux0A==} - source-list-map@2.0.1: - resolution: {integrity: sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==} - source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -3775,8 +3441,8 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} - std-env@3.10.0: - resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + std-env@4.0.0: + resolution: {integrity: sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==} string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} @@ -3850,9 +3516,6 @@ packages: engines: {node: '>=16 || 14 >=14.17'} hasBin: true - superstruct@0.10.13: - resolution: {integrity: sha512-W4SitSZ9MOyMPbHreoZVEneSZyPEeNGbdfJo/7FkJyRs/M3wQRFzq+t3S/NBwlrFSWdx1ONLjLb9pB+UKe4IqQ==} - supports-color@10.2.2: resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} engines: {node: '>=18'} @@ -3861,10 +3524,6 @@ packages: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} - supports-color@8.1.1: - resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} - engines: {node: '>=10'} - supports-hyperlinks@4.4.0: resolution: {integrity: sha512-UKbpT93hN5Nr9go5UY7bopIB9YQlMz9nm/ct4IXt/irb5YRkn9WaqrOBJGZ5Pwvsd5FQzSVeYlGdXoCAPQZrPg==} engines: {node: '>=20'} @@ -3907,26 +3566,6 @@ packages: engines: {node: '>=14.0.0'} hasBin: true - tapable@2.3.0: - resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} - engines: {node: '>=6'} - - terser-webpack-plugin@5.3.17: - resolution: {integrity: sha512-YR7PtUp6GMU91BgSJmlaX/rS2lGDbAF7D+Wtq7hRO+MiljNmodYvqslzCFiYVAgW+Qoaaia/QUIP4lGXufjdZw==} - engines: {node: '>= 10.13.0'} - peerDependencies: - '@swc/core': '*' - esbuild: '*' - uglify-js: '*' - webpack: ^5.1.0 - peerDependenciesMeta: - '@swc/core': - optional: true - esbuild: - optional: true - uglify-js: - optional: true - terser@5.46.0: resolution: {integrity: sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==} engines: {node: '>=10'} @@ -4058,20 +3697,21 @@ packages: vanilla-colorful@0.7.2: resolution: {integrity: sha512-z2YZusTFC6KnLERx1cgoIRX2CjPRP0W75N+3CC6gbvdX5Ch47rZkEMGO2Xnf+IEmi3RiFLxS18gayMA27iU7Kg==} - vite-string-plugin@2.0.1: - resolution: {integrity: sha512-L5B86yQkYrqH5d966w1vI91B0d+0vmICgB6tqjINvtBIGU9qhFY7izqjytED/ApggFC4QTDWNjfF6nWMqY/fQg==} + vite-string-plugin@2.0.2: + resolution: {integrity: sha512-pHU9lZuUoMSYyZixdn2XBYko9IAhk3dr41CG6VsXrjB+wN2th06SZsO9mJm6+2NhKBJKNfRERaRej8TBcoq9tQ==} peerDependencies: vite: '*' - vite@7.3.1: - resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} + vite@8.0.0: + resolution: {integrity: sha512-fPGaRNj9Zytaf8LEiBhY7Z6ijnFKdzU/+mL8EFBaKr7Vw1/FWcTBAMW0wLPJAGMPX38ZPVCVgLceWiEqeoqL2Q==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.0.0-alpha.31 + esbuild: ^0.27.0 jiti: '>=1.21.0' less: ^4.0.0 - lightningcss: ^1.21.0 sass: ^1.70.0 sass-embedded: ^1.70.0 stylus: '>=0.54.8' @@ -4082,12 +3722,14 @@ packages: peerDependenciesMeta: '@types/node': optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true jiti: optional: true less: optional: true - lightningcss: - optional: true sass: optional: true sass-embedded: @@ -4103,20 +3745,21 @@ packages: yaml: optional: true - vitest@4.0.18: - resolution: {integrity: sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==} + vitest@4.1.0: + resolution: {integrity: sha512-YbDrMF9jM2Lqc++2530UourxZHmkKLxrs4+mYhEwqWS97WJ7wOYEkcr+QfRgJ3PW9wz3odRijLZjHEaRLTNbqw==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@opentelemetry/api': ^1.9.0 '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/browser-playwright': 4.0.18 - '@vitest/browser-preview': 4.0.18 - '@vitest/browser-webdriverio': 4.0.18 - '@vitest/ui': 4.0.18 + '@vitest/browser-playwright': 4.1.0 + '@vitest/browser-preview': 4.1.0 + '@vitest/browser-webdriverio': 4.1.0 + '@vitest/ui': 4.1.0 happy-dom: '*' jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0-0 peerDependenciesMeta: '@edge-runtime/vm': optional: true @@ -4172,18 +3815,6 @@ packages: peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 - vue-loader@17.4.2: - resolution: {integrity: sha512-yTKOA4R/VN4jqjw4y5HrynFL8AK0Z3/Jt7eOJXEitsm0GMRHDBjCfCiuTiLP7OESvsZYo2pATCWhDqxC5ZrM6w==} - peerDependencies: - '@vue/compiler-sfc': '*' - vue: '*' - webpack: ^4.1.0 || ^5.0.0-0 - peerDependenciesMeta: - '@vue/compiler-sfc': - optional: true - vue: - optional: true - vue-tsc@3.2.5: resolution: {integrity: sha512-/htfTCMluQ+P2FISGAooul8kO4JMheOTCbCy4M6dYnYYjqLe3BExZudAua6MSIKSFYQtFOYAll7XobYwcpokGA==} hasBin: true @@ -4198,48 +3829,9 @@ packages: typescript: optional: true - watchpack@2.5.1: - resolution: {integrity: sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==} - engines: {node: '>=10.13.0'} - webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} - webpack-cli@6.0.1: - resolution: {integrity: sha512-MfwFQ6SfwinsUVi0rNJm7rHZ31GyTcpVE5pgVA3hwFRb7COD4TzjUUwhGWKfO50+xdc2MQPuEBBJoqIMGt3JDw==} - engines: {node: '>=18.12.0'} - hasBin: true - peerDependencies: - webpack: ^5.82.0 - webpack-bundle-analyzer: '*' - webpack-dev-server: '*' - peerDependenciesMeta: - webpack-bundle-analyzer: - optional: true - webpack-dev-server: - optional: true - - webpack-merge@6.0.1: - resolution: {integrity: sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg==} - engines: {node: '>=18.0.0'} - - webpack-sources@1.4.3: - resolution: {integrity: sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==} - - webpack-sources@3.3.4: - resolution: {integrity: sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==} - engines: {node: '>=10.13.0'} - - webpack@5.105.4: - resolution: {integrity: sha512-jTywjboN9aHxFlToqb0K0Zs9SbBoW4zRUlGzI2tYNxVYcEi/IPpn+Xi4ye5jTLvX2YeLuic/IvxNot+Q1jMoOw==} - engines: {node: '>=10.13.0'} - hasBin: true - peerDependencies: - webpack-cli: '*' - peerDependenciesMeta: - webpack-cli: - optional: true - whatwg-mimetype@3.0.0: resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} engines: {node: '>=12'} @@ -4261,9 +3853,6 @@ packages: engines: {node: '>=8'} hasBin: true - wildcard@2.0.1: - resolution: {integrity: sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==} - word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} @@ -4272,13 +3861,6 @@ packages: resolution: {integrity: sha512-SGcvg80f0wUy2/fXES19feHMz8E0JoXv2uNgHOu4Dgi2OrCy1lqwFYEJz1BLbDI0exjPMe/ZdzZ/YpGECBG/aQ==} engines: {node: '>=20'} - wrap-ansi@6.2.0: - resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} - engines: {node: '>=8'} - - wrappy@1.0.2: - resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - write-file-atomic@7.0.1: resolution: {integrity: sha512-OTIk8iR8/aCRWBqvxrzxR0hgxWpnYBblY1S5hDWBQfk/VFmJwzmJgQFN3WsoUKHISv2eAwe+PpbUzyL1CKTLXg==} engines: {node: ^20.17.0 || >=22.9.0} @@ -4459,8 +4041,6 @@ snapshots: dependencies: postcss-selector-parser: 7.1.1 - '@discoveryjs/json-ext@0.6.3': {} - '@emnapi/core@1.8.1': dependencies: '@emnapi/wasi-threads': 1.1.0 @@ -4477,84 +4057,6 @@ snapshots: tslib: 2.8.1 optional: true - '@esbuild/aix-ppc64@0.27.3': - optional: true - - '@esbuild/android-arm64@0.27.3': - optional: true - - '@esbuild/android-arm@0.27.3': - optional: true - - '@esbuild/android-x64@0.27.3': - optional: true - - '@esbuild/darwin-arm64@0.27.3': - optional: true - - '@esbuild/darwin-x64@0.27.3': - optional: true - - '@esbuild/freebsd-arm64@0.27.3': - optional: true - - '@esbuild/freebsd-x64@0.27.3': - optional: true - - '@esbuild/linux-arm64@0.27.3': - optional: true - - '@esbuild/linux-arm@0.27.3': - optional: true - - '@esbuild/linux-ia32@0.27.3': - optional: true - - '@esbuild/linux-loong64@0.27.3': - optional: true - - '@esbuild/linux-mips64el@0.27.3': - optional: true - - '@esbuild/linux-ppc64@0.27.3': - optional: true - - '@esbuild/linux-riscv64@0.27.3': - optional: true - - '@esbuild/linux-s390x@0.27.3': - optional: true - - '@esbuild/linux-x64@0.27.3': - optional: true - - '@esbuild/netbsd-arm64@0.27.3': - optional: true - - '@esbuild/netbsd-x64@0.27.3': - optional: true - - '@esbuild/openbsd-arm64@0.27.3': - optional: true - - '@esbuild/openbsd-x64@0.27.3': - optional: true - - '@esbuild/openharmony-arm64@0.27.3': - optional: true - - '@esbuild/sunos-x64@0.27.3': - optional: true - - '@esbuild/win32-arm64@0.27.3': - optional: true - - '@esbuild/win32-ia32@0.27.3': - optional: true - - '@esbuild/win32-x64@0.27.3': - optional: true - '@eslint-community/eslint-plugin-eslint-comments@4.7.1(eslint@9.39.2(jiti@2.6.1))': dependencies: escape-string-regexp: 4.0.0 @@ -4667,6 +4169,7 @@ snapshots: dependencies: '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 + optional: true '@jridgewell/sourcemap-codec@1.5.5': {} @@ -4708,6 +4211,13 @@ snapshots: '@tybys/wasm-util': 0.10.1 optional: true + '@napi-rs/wasm-runtime@1.1.1': + dependencies: + '@emnapi/core': 1.8.1 + '@emnapi/runtime': 1.8.1 + '@tybys/wasm-util': 0.10.1 + optional: true + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -4770,6 +4280,10 @@ snapshots: dependencies: '@nolyfill/shared': 1.0.44 + '@oxc-project/runtime@0.115.0': {} + + '@oxc-project/types@0.115.0': {} + '@pkgr/core@0.2.9': {} '@playwright/test@1.58.2': @@ -4784,8 +4298,57 @@ snapshots: '@resvg/resvg-wasm@2.6.2': {} + '@rolldown/binding-android-arm64@1.0.0-rc.9': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.0-rc.9': + optional: true + + '@rolldown/binding-darwin-x64@1.0.0-rc.9': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.0-rc.9': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.9': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.9': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.9': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.9': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.9': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.9': + optional: true + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.9': + optional: true + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.9': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.9': + dependencies: + '@napi-rs/wasm-runtime': 1.1.1 + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.9': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.9': + optional: true + '@rolldown/pluginutils@1.0.0-rc.2': {} + '@rolldown/pluginutils@1.0.0-rc.9': {} + '@rollup/rollup-android-arm-eabi@4.59.0': optional: true @@ -4915,19 +4478,6 @@ snapshots: '@swc/helpers@0.2.14': {} - '@techknowlogick/license-checker-webpack-plugin@0.3.0(webpack@5.105.4)': - dependencies: - glob: 7.2.3 - lodash: 4.17.23 - minimatch: 3.1.5 - semver: 6.3.1 - spdx-expression-validate: 2.0.0 - spdx-satisfies: 5.0.1 - superstruct: 0.10.13 - webpack: 5.105.4(webpack-cli@6.0.1) - webpack-sources: 1.4.3 - wrap-ansi: 6.2.0 - '@tybys/wasm-util@0.10.1': dependencies: tslib: 2.8.1 @@ -5067,17 +4617,13 @@ snapshots: '@types/dropzone@5.7.9': dependencies: - '@types/jquery': 4.0.0 - - '@types/eslint-scope@3.7.7': - dependencies: - '@types/eslint': 9.6.1 - '@types/estree': 1.0.8 + '@types/jquery': 4.0.0 '@types/eslint@9.6.1': dependencies: '@types/estree': 1.0.8 '@types/json-schema': 7.0.15 + optional: true '@types/esrecurse@4.3.1': {} @@ -5280,60 +4826,62 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.11.1': optional: true - '@vitejs/plugin-vue@6.0.4(vite@7.3.1(@types/node@25.3.5)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2))(vue@3.5.29(typescript@5.9.3))': + '@vitejs/plugin-vue@6.0.5(vite@8.0.0(@types/node@25.3.5)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2))(vue@3.5.29(typescript@5.9.3))': dependencies: '@rolldown/pluginutils': 1.0.0-rc.2 - vite: 7.3.1(@types/node@25.3.5)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2) + vite: 8.0.0(@types/node@25.3.5)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2) vue: 3.5.29(typescript@5.9.3) - '@vitest/eslint-plugin@1.6.9(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)(vitest@4.0.18(@types/node@25.3.5)(happy-dom@20.8.3)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2))': + '@vitest/eslint-plugin@1.6.9(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)(vitest@4.1.0(@types/node@25.3.5)(happy-dom@20.8.3)(vite@8.0.0(@types/node@25.3.5)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2)))': dependencies: '@typescript-eslint/scope-manager': 8.56.1 '@typescript-eslint/utils': 8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.2(jiti@2.6.1) optionalDependencies: typescript: 5.9.3 - vitest: 4.0.18(@types/node@25.3.5)(happy-dom@20.8.3)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2) + vitest: 4.1.0(@types/node@25.3.5)(happy-dom@20.8.3)(vite@8.0.0(@types/node@25.3.5)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2)) transitivePeerDependencies: - supports-color - '@vitest/expect@4.0.18': + '@vitest/expect@4.1.0': dependencies: '@standard-schema/spec': 1.1.0 '@types/chai': 5.2.3 - '@vitest/spy': 4.0.18 - '@vitest/utils': 4.0.18 + '@vitest/spy': 4.1.0 + '@vitest/utils': 4.1.0 chai: 6.2.2 tinyrainbow: 3.0.3 - '@vitest/mocker@4.0.18(vite@7.3.1(@types/node@25.3.5)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2))': + '@vitest/mocker@4.1.0(vite@8.0.0(@types/node@25.3.5)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2))': dependencies: - '@vitest/spy': 4.0.18 + '@vitest/spy': 4.1.0 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.1(@types/node@25.3.5)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2) + vite: 8.0.0(@types/node@25.3.5)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2) - '@vitest/pretty-format@4.0.18': + '@vitest/pretty-format@4.1.0': dependencies: tinyrainbow: 3.0.3 - '@vitest/runner@4.0.18': + '@vitest/runner@4.1.0': dependencies: - '@vitest/utils': 4.0.18 + '@vitest/utils': 4.1.0 pathe: 2.0.3 - '@vitest/snapshot@4.0.18': + '@vitest/snapshot@4.1.0': dependencies: - '@vitest/pretty-format': 4.0.18 + '@vitest/pretty-format': 4.1.0 + '@vitest/utils': 4.1.0 magic-string: 0.30.21 pathe: 2.0.3 - '@vitest/spy@4.0.18': {} + '@vitest/spy@4.1.0': {} - '@vitest/utils@4.0.18': + '@vitest/utils@4.1.0': dependencies: - '@vitest/pretty-format': 4.0.18 + '@vitest/pretty-format': 4.1.0 + convert-source-map: 2.0.0 tinyrainbow: 3.0.3 '@volar/language-core@2.4.28': @@ -5412,124 +4960,12 @@ snapshots: '@vue/shared@3.5.29': {} - '@webassemblyjs/ast@1.14.1': - dependencies: - '@webassemblyjs/helper-numbers': 1.13.2 - '@webassemblyjs/helper-wasm-bytecode': 1.13.2 - - '@webassemblyjs/floating-point-hex-parser@1.13.2': {} - - '@webassemblyjs/helper-api-error@1.13.2': {} - - '@webassemblyjs/helper-buffer@1.14.1': {} - - '@webassemblyjs/helper-numbers@1.13.2': - dependencies: - '@webassemblyjs/floating-point-hex-parser': 1.13.2 - '@webassemblyjs/helper-api-error': 1.13.2 - '@xtuc/long': 4.2.2 - - '@webassemblyjs/helper-wasm-bytecode@1.13.2': {} - - '@webassemblyjs/helper-wasm-section@1.14.1': - dependencies: - '@webassemblyjs/ast': 1.14.1 - '@webassemblyjs/helper-buffer': 1.14.1 - '@webassemblyjs/helper-wasm-bytecode': 1.13.2 - '@webassemblyjs/wasm-gen': 1.14.1 - - '@webassemblyjs/ieee754@1.13.2': - dependencies: - '@xtuc/ieee754': 1.2.0 - - '@webassemblyjs/leb128@1.13.2': - dependencies: - '@xtuc/long': 4.2.2 - - '@webassemblyjs/utf8@1.13.2': {} - - '@webassemblyjs/wasm-edit@1.14.1': - dependencies: - '@webassemblyjs/ast': 1.14.1 - '@webassemblyjs/helper-buffer': 1.14.1 - '@webassemblyjs/helper-wasm-bytecode': 1.13.2 - '@webassemblyjs/helper-wasm-section': 1.14.1 - '@webassemblyjs/wasm-gen': 1.14.1 - '@webassemblyjs/wasm-opt': 1.14.1 - '@webassemblyjs/wasm-parser': 1.14.1 - '@webassemblyjs/wast-printer': 1.14.1 - - '@webassemblyjs/wasm-gen@1.14.1': - dependencies: - '@webassemblyjs/ast': 1.14.1 - '@webassemblyjs/helper-wasm-bytecode': 1.13.2 - '@webassemblyjs/ieee754': 1.13.2 - '@webassemblyjs/leb128': 1.13.2 - '@webassemblyjs/utf8': 1.13.2 - - '@webassemblyjs/wasm-opt@1.14.1': - dependencies: - '@webassemblyjs/ast': 1.14.1 - '@webassemblyjs/helper-buffer': 1.14.1 - '@webassemblyjs/wasm-gen': 1.14.1 - '@webassemblyjs/wasm-parser': 1.14.1 - - '@webassemblyjs/wasm-parser@1.14.1': - dependencies: - '@webassemblyjs/ast': 1.14.1 - '@webassemblyjs/helper-api-error': 1.13.2 - '@webassemblyjs/helper-wasm-bytecode': 1.13.2 - '@webassemblyjs/ieee754': 1.13.2 - '@webassemblyjs/leb128': 1.13.2 - '@webassemblyjs/utf8': 1.13.2 - - '@webassemblyjs/wast-printer@1.14.1': - dependencies: - '@webassemblyjs/ast': 1.14.1 - '@xtuc/long': 4.2.2 - - '@webpack-cli/configtest@3.0.1(webpack-cli@6.0.1)(webpack@5.105.4)': - dependencies: - webpack: 5.105.4(webpack-cli@6.0.1) - webpack-cli: 6.0.1(webpack@5.105.4) - - '@webpack-cli/info@3.0.1(webpack-cli@6.0.1)(webpack@5.105.4)': - dependencies: - webpack: 5.105.4(webpack-cli@6.0.1) - webpack-cli: 6.0.1(webpack@5.105.4) - - '@webpack-cli/serve@3.0.1(webpack-cli@6.0.1)(webpack@5.105.4)': - dependencies: - webpack: 5.105.4(webpack-cli@6.0.1) - webpack-cli: 6.0.1(webpack@5.105.4) - - '@xtuc/ieee754@1.2.0': {} - - '@xtuc/long@4.2.2': {} - - acorn-import-phases@1.0.4(acorn@8.16.0): - dependencies: - acorn: 8.16.0 - acorn-jsx@5.3.2(acorn@8.16.0): dependencies: acorn: 8.16.0 acorn@8.16.0: {} - add-asset-webpack-plugin@3.1.1(webpack@5.105.4): - optionalDependencies: - webpack: 5.105.4(webpack-cli@6.0.1) - - ajv-formats@2.1.1(ajv@8.18.0): - optionalDependencies: - ajv: 8.18.0 - - ajv-keywords@5.1.0(ajv@8.18.0): - dependencies: - ajv: 8.18.0 - fast-deep-equal: 3.1.3 - ajv@6.14.0: dependencies: fast-deep-equal: 3.1.3 @@ -5597,8 +5033,6 @@ snapshots: baseline-browser-mapping@2.10.0: {} - big.js@5.2.2: {} - binary-extensions@2.3.0: {} boolbase@1.0.0: {} @@ -5624,7 +5058,8 @@ snapshots: node-releases: 2.0.36 update-browserslist-db: 1.2.3(browserslist@4.28.1) - buffer-from@1.1.2: {} + buffer-from@1.1.2: + optional: true buffer@5.7.1: dependencies: @@ -5709,8 +5144,6 @@ snapshots: chroma-js@3.2.0: {} - chrome-trace-event@1.0.4: {} - ci-info@4.4.0: {} citeproc@2.4.63: {} @@ -5721,12 +5154,6 @@ snapshots: clippie@4.1.10: {} - clone-deep@4.0.1: - dependencies: - is-plain-object: 2.0.4 - kind-of: 6.0.3 - shallow-clone: 3.0.1 - codemirror-spell-checker@1.1.2: dependencies: typo-js: 1.3.1 @@ -5741,15 +5168,12 @@ snapshots: colord@2.9.3: {} - colorette@2.0.20: {} - commander@11.1.0: {} - commander@12.1.0: {} - commander@14.0.3: {} - commander@2.20.3: {} + commander@2.20.3: + optional: true commander@4.1.1: {} @@ -5759,12 +5183,16 @@ snapshots: comment-parser@1.4.5: {} + commenting@1.1.0: {} + compare-versions@6.1.1: {} concat-map@0.0.1: {} confbox@0.1.8: {} + convert-source-map@2.0.0: {} + core-js-compat@3.48.0: dependencies: browserslist: 4.28.1 @@ -5798,19 +5226,6 @@ snapshots: css-functions-list@3.3.3: {} - css-loader@7.1.4(webpack@5.105.4): - dependencies: - icss-utils: 5.1.0(postcss@8.5.8) - postcss: 8.5.8 - postcss-modules-extract-imports: 3.1.0(postcss@8.5.8) - postcss-modules-local-by-default: 4.2.0(postcss@8.5.8) - postcss-modules-scope: 3.2.1(postcss@8.5.8) - postcss-modules-values: 4.0.0(postcss@8.5.8) - postcss-value-parser: 4.2.0 - semver: 7.7.4 - optionalDependencies: - webpack: 5.105.4(webpack-cli@6.0.1) - css-select@5.2.2: dependencies: boolbase: 1.0.0 @@ -6054,6 +5469,8 @@ snapshots: dequal@2.0.3: {} + detect-libc@2.1.2: {} + devlop@1.1.0: dependencies: dequal: 2.0.3 @@ -6115,66 +5532,18 @@ snapshots: emoji-regex@9.2.2: {} - emojis-list@3.0.0: {} - - enhanced-resolve@5.20.0: - dependencies: - graceful-fs: 4.2.11 - tapable: 2.3.0 - entities@4.5.0: {} entities@7.0.1: {} env-paths@2.2.1: {} - envinfo@7.21.0: {} - error-ex@1.3.4: dependencies: is-arrayish: 0.2.1 - es-module-lexer@1.7.0: {} - es-module-lexer@2.0.0: {} - esbuild-loader@4.4.2(webpack@5.105.4): - dependencies: - esbuild: 0.27.3 - get-tsconfig: 4.13.6 - loader-utils: 2.0.4 - webpack: 5.105.4(webpack-cli@6.0.1) - webpack-sources: 1.4.3 - - esbuild@0.27.3: - optionalDependencies: - '@esbuild/aix-ppc64': 0.27.3 - '@esbuild/android-arm': 0.27.3 - '@esbuild/android-arm64': 0.27.3 - '@esbuild/android-x64': 0.27.3 - '@esbuild/darwin-arm64': 0.27.3 - '@esbuild/darwin-x64': 0.27.3 - '@esbuild/freebsd-arm64': 0.27.3 - '@esbuild/freebsd-x64': 0.27.3 - '@esbuild/linux-arm': 0.27.3 - '@esbuild/linux-arm64': 0.27.3 - '@esbuild/linux-ia32': 0.27.3 - '@esbuild/linux-loong64': 0.27.3 - '@esbuild/linux-mips64el': 0.27.3 - '@esbuild/linux-ppc64': 0.27.3 - '@esbuild/linux-riscv64': 0.27.3 - '@esbuild/linux-s390x': 0.27.3 - '@esbuild/linux-x64': 0.27.3 - '@esbuild/netbsd-arm64': 0.27.3 - '@esbuild/netbsd-x64': 0.27.3 - '@esbuild/openbsd-arm64': 0.27.3 - '@esbuild/openbsd-x64': 0.27.3 - '@esbuild/openharmony-arm64': 0.27.3 - '@esbuild/sunos-x64': 0.27.3 - '@esbuild/win32-arm64': 0.27.3 - '@esbuild/win32-ia32': 0.27.3 - '@esbuild/win32-x64': 0.27.3 - escalade@3.2.0: {} escape-string-regexp@1.0.5: {} @@ -6448,11 +5817,6 @@ snapshots: eslint-rule-documentation@1.0.23: {} - eslint-scope@5.1.1: - dependencies: - esrecurse: 4.3.0 - estraverse: 4.3.0 - eslint-scope@8.4.0: dependencies: esrecurse: 4.3.0 @@ -6532,8 +5896,6 @@ snapshots: dependencies: estraverse: 5.3.0 - estraverse@4.3.0: {} - estraverse@5.3.0: {} estree-walker@2.0.2: {} @@ -6600,11 +5962,6 @@ snapshots: find-up-simple@1.0.1: {} - find-up@4.1.0: - dependencies: - locate-path: 5.0.0 - path-exists: 4.0.0 - find-up@5.0.0: dependencies: locate-path: 6.0.0 @@ -6621,12 +5978,8 @@ snapshots: flatted: 3.3.4 hookified: 1.15.1 - flat@5.0.2: {} - flatted@3.3.4: {} - fs.realpath@1.0.0: {} - fsevents@2.3.2: optional: true @@ -6649,17 +6002,6 @@ snapshots: dependencies: is-glob: 4.0.3 - glob-to-regexp@0.4.1: {} - - glob@7.2.3: - dependencies: - fs.realpath: 1.0.0 - inflight: 1.0.6 - inherits: 2.0.4 - minimatch: 3.1.5 - once: 1.4.0 - path-is-absolute: 1.0.1 - global-modules@2.0.0: dependencies: global-prefix: 3.0.0 @@ -6687,8 +6029,6 @@ snapshots: globjoin@0.1.4: {} - graceful-fs@4.2.11: {} - hachure-fill@0.5.2: {} hammerjs@2.0.8: {} @@ -6709,8 +6049,6 @@ snapshots: has-flag@5.0.1: {} - hash-sum@2.0.0: {} - hashery@1.5.0: dependencies: hookified: 1.15.1 @@ -6732,10 +6070,6 @@ snapshots: dependencies: safer-buffer: '@nolyfill/safer-buffer@1.0.44' - icss-utils@5.1.0(postcss@8.5.8): - dependencies: - postcss: 8.5.8 - idiomorph@0.7.4: {} ieee754@1.2.1: {} @@ -6749,24 +6083,12 @@ snapshots: parent-module: 1.0.1 resolve-from: 4.0.0 - import-local@3.2.0: - dependencies: - pkg-dir: 4.2.0 - resolve-cwd: 3.0.0 - import-meta-resolve@4.2.0: {} imurmurhash@0.1.4: {} indent-string@5.0.0: {} - inflight@1.0.6: - dependencies: - once: 1.4.0 - wrappy: 1.0.2 - - inherits@2.0.4: {} - ini@1.3.8: {} ini@4.1.3: {} @@ -6775,8 +6097,6 @@ snapshots: internmap@2.0.3: {} - interpret@3.1.1: {} - is-alphabetical@2.0.1: {} is-alphanumerical@2.0.1: @@ -6816,10 +6136,6 @@ snapshots: is-path-inside@4.0.0: {} - is-plain-object@2.0.4: - dependencies: - isobject: 3.0.1 - is-plain-object@5.0.0: {} is-potential-custom-element-name@1.0.1: {} @@ -6830,14 +6146,6 @@ snapshots: isexe@2.0.0: {} - isobject@3.0.1: {} - - jest-worker@27.5.1: - dependencies: - '@types/node': 25.3.5 - merge-stream: 2.0.0 - supports-color: 8.1.1 - jiti@1.21.7: {} jiti@2.6.1: {} @@ -6872,8 +6180,6 @@ snapshots: dependencies: minimist: 1.2.8 - json5@2.2.3: {} - jsonc-parser@3.3.1: {} jsonpointer@5.0.1: {} @@ -6932,25 +6238,62 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 - lilconfig@3.1.3: {} + lightningcss-android-arm64@1.32.0: + optional: true - lines-and-columns@1.2.4: {} + lightningcss-darwin-arm64@1.32.0: + optional: true - linkify-it@5.0.0: - dependencies: - uc.micro: 2.1.0 + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true - loader-runner@4.3.1: {} + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true - loader-utils@2.0.4: + lightningcss@1.32.0: dependencies: - big.js: 5.2.2 - emojis-list: 3.0.0 - json5: 2.2.3 + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + + lilconfig@3.1.3: {} + + lines-and-columns@1.2.4: {} - locate-path@5.0.0: + linkify-it@5.0.0: dependencies: - p-locate: 4.1.0 + uc.micro: 2.1.0 locate-path@6.0.0: dependencies: @@ -7039,8 +6382,6 @@ snapshots: meow@14.1.0: {} - merge-stream@2.0.0: {} - merge2@1.4.1: {} mermaid@11.12.3: @@ -7243,18 +6584,6 @@ snapshots: braces: 3.0.3 picomatch: 2.3.1 - mime-db@1.52.0: {} - - mime-types@2.1.35: - dependencies: - mime-db: 1.52.0 - - mini-css-extract-plugin@2.10.0(webpack@5.105.4): - dependencies: - schema-utils: 4.3.3 - tapable: 2.3.0 - webpack: 5.105.4(webpack-cli@6.0.1) - minimatch@10.2.4: dependencies: brace-expansion: 5.0.4 @@ -7272,11 +6601,7 @@ snapshots: pkg-types: 1.3.1 ufo: 1.6.3 - monaco-editor-webpack-plugin@7.1.1(monaco-editor@0.55.1)(webpack@5.105.4): - dependencies: - loader-utils: 2.0.4 - monaco-editor: 0.55.1 - webpack: 5.105.4(webpack-cli@6.0.1) + moment@2.30.1: {} monaco-editor@0.55.1: dependencies: @@ -7303,8 +6628,6 @@ snapshots: natural-compare@1.4.0: {} - neo-async@2.6.2: {} - node-fetch@2.6.13: dependencies: whatwg-url: 5.0.0 @@ -7329,10 +6652,6 @@ snapshots: obug@2.1.1: {} - once@1.4.0: - dependencies: - wrappy: 1.0.2 - online-3d-viewer@0.18.0: dependencies: '@simonwep/pickr': 1.9.0 @@ -7348,26 +6667,18 @@ snapshots: type-check: 0.4.0 word-wrap: 1.2.5 - p-limit@2.3.0: - dependencies: - p-try: 2.2.0 - p-limit@3.1.0: dependencies: yocto-queue: 0.1.0 - p-locate@4.1.0: - dependencies: - p-limit: 2.3.0 - p-locate@5.0.0: dependencies: p-limit: 3.1.0 - p-try@2.2.0: {} - package-manager-detector@1.6.0: {} + package-name-regex@2.0.6: {} + parent-module@1.0.1: dependencies: callsites: 3.1.0 @@ -7395,8 +6706,6 @@ snapshots: path-exists@4.0.0: {} - path-is-absolute@1.0.1: {} - path-key@3.1.1: {} path-parse@1.0.7: {} @@ -7417,10 +6726,6 @@ snapshots: pirates@4.0.7: {} - pkg-dir@4.2.0: - dependencies: - find-up: 4.1.0 - pkg-types@1.3.1: dependencies: confbox: 0.1.8 @@ -7470,38 +6775,6 @@ snapshots: optionalDependencies: postcss: 8.5.8 - postcss-loader@8.2.1(postcss@8.5.8)(typescript@5.9.3)(webpack@5.105.4): - dependencies: - cosmiconfig: 9.0.1(typescript@5.9.3) - jiti: 2.6.1 - postcss: 8.5.8 - semver: 7.7.4 - optionalDependencies: - webpack: 5.105.4(webpack-cli@6.0.1) - transitivePeerDependencies: - - typescript - - postcss-modules-extract-imports@3.1.0(postcss@8.5.8): - dependencies: - postcss: 8.5.8 - - postcss-modules-local-by-default@4.2.0(postcss@8.5.8): - dependencies: - icss-utils: 5.1.0(postcss@8.5.8) - postcss: 8.5.8 - postcss-selector-parser: 7.1.1 - postcss-value-parser: 4.2.0 - - postcss-modules-scope@3.2.1(postcss@8.5.8): - dependencies: - postcss: 8.5.8 - postcss-selector-parser: 7.1.1 - - postcss-modules-values@4.0.0(postcss@8.5.8): - dependencies: - icss-utils: 5.1.0(postcss@8.5.8) - postcss: 8.5.8 - postcss-nested@6.2.0(postcss@8.5.8): dependencies: postcss: 8.5.8 @@ -7559,10 +6832,6 @@ snapshots: dependencies: picomatch: 2.3.1 - rechoir@0.8.0: - dependencies: - resolve: 1.22.11 - refa@0.12.1: dependencies: '@eslint-community/regexpp': 4.12.2 @@ -7582,14 +6851,8 @@ snapshots: require-from-string@2.0.2: {} - resolve-cwd@3.0.0: - dependencies: - resolve-from: 5.0.0 - resolve-from@4.0.0: {} - resolve-from@5.0.0: {} - resolve-pkg-maps@1.0.0: {} resolve@1.22.11: @@ -7602,6 +6865,41 @@ snapshots: robust-predicates@3.0.2: {} + rolldown@1.0.0-rc.9: + dependencies: + '@oxc-project/types': 0.115.0 + '@rolldown/pluginutils': 1.0.0-rc.9 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.0-rc.9 + '@rolldown/binding-darwin-arm64': 1.0.0-rc.9 + '@rolldown/binding-darwin-x64': 1.0.0-rc.9 + '@rolldown/binding-freebsd-x64': 1.0.0-rc.9 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.9 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.9 + '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.9 + '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.9 + '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.9 + '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.9 + '@rolldown/binding-linux-x64-musl': 1.0.0-rc.9 + '@rolldown/binding-openharmony-arm64': 1.0.0-rc.9 + '@rolldown/binding-wasm32-wasi': 1.0.0-rc.9 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.9 + '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.9 + + rollup-plugin-license@3.7.0(picomatch@4.0.3)(rollup@4.59.0): + dependencies: + commenting: 1.1.0 + fdir: 6.5.0(picomatch@4.0.3) + lodash: 4.17.23 + magic-string: 0.30.21 + moment: 2.30.1 + package-name-regex: 2.0.6 + rollup: 4.59.0 + spdx-expression-validate: 2.0.0 + spdx-satisfies: 5.0.1 + transitivePeerDependencies: + - picomatch + rollup@4.59.0: dependencies: '@types/estree': 1.0.8 @@ -7655,13 +6953,6 @@ snapshots: sax@1.5.0: {} - schema-utils@4.3.3: - dependencies: - '@types/json-schema': 7.0.15 - ajv: 8.18.0 - ajv-formats: 2.1.1(ajv@8.18.0) - ajv-keywords: 5.1.0(ajv@8.18.0) - scslre@0.3.0: dependencies: '@eslint-community/regexpp': 4.12.2 @@ -7678,10 +6969,6 @@ snapshots: seroval@1.5.0: {} - shallow-clone@3.0.1: - dependencies: - kind-of: 6.0.3 - shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -7716,16 +7003,16 @@ snapshots: sortablejs@1.15.7: {} - source-list-map@2.0.1: {} - source-map-js@1.2.1: {} source-map-support@0.5.21: dependencies: buffer-from: 1.1.2 source-map: 0.6.1 + optional: true - source-map@0.6.1: {} + source-map@0.6.1: + optional: true spdx-compare@1.0.0: dependencies: @@ -7762,7 +7049,7 @@ snapshots: stackback@0.0.2: {} - std-env@3.10.0: {} + std-env@4.0.0: {} string-width@4.2.3: dependencies: @@ -7869,18 +7156,12 @@ snapshots: tinyglobby: 0.2.15 ts-interface-checker: 0.1.13 - superstruct@0.10.13: {} - supports-color@10.2.2: {} supports-color@7.2.0: dependencies: has-flag: 4.0.0 - supports-color@8.1.1: - dependencies: - has-flag: 4.0.0 - supports-hyperlinks@4.4.0: dependencies: has-flag: 5.0.1 @@ -7957,22 +7238,13 @@ snapshots: transitivePeerDependencies: - ts-node - tapable@2.3.0: {} - - terser-webpack-plugin@5.3.17(webpack@5.105.4): - dependencies: - '@jridgewell/trace-mapping': 0.3.31 - jest-worker: 27.5.1 - schema-utils: 4.3.3 - terser: 5.46.0 - webpack: 5.105.4(webpack-cli@6.0.1) - terser@5.46.0: dependencies: '@jridgewell/source-map': 0.3.11 acorn: 8.16.0 commander: 2.20.3 source-map-support: 0.5.21 + optional: true thenify-all@1.6.0: dependencies: @@ -8100,17 +7372,17 @@ snapshots: vanilla-colorful@0.7.2: {} - vite-string-plugin@2.0.1(vite@7.3.1(@types/node@25.3.5)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2)): + vite-string-plugin@2.0.2(vite@8.0.0(@types/node@25.3.5)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2)): dependencies: - vite: 7.3.1(@types/node@25.3.5)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2) + vite: 8.0.0(@types/node@25.3.5)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2) - vite@7.3.1(@types/node@25.3.5)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2): + vite@8.0.0(@types/node@25.3.5)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2): dependencies: - esbuild: 0.27.3 - fdir: 6.5.0(picomatch@4.0.3) + '@oxc-project/runtime': 0.115.0 + lightningcss: 1.32.0 picomatch: 4.0.3 postcss: 8.5.8 - rollup: 4.59.0 + rolldown: 1.0.0-rc.9 tinyglobby: 0.2.15 optionalDependencies: '@types/node': 25.3.5 @@ -8119,43 +7391,33 @@ snapshots: terser: 5.46.0 yaml: 2.8.2 - vitest@4.0.18(@types/node@25.3.5)(happy-dom@20.8.3)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2): + vitest@4.1.0(@types/node@25.3.5)(happy-dom@20.8.3)(vite@8.0.0(@types/node@25.3.5)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2)): dependencies: - '@vitest/expect': 4.0.18 - '@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@25.3.5)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2)) - '@vitest/pretty-format': 4.0.18 - '@vitest/runner': 4.0.18 - '@vitest/snapshot': 4.0.18 - '@vitest/spy': 4.0.18 - '@vitest/utils': 4.0.18 - es-module-lexer: 1.7.0 + '@vitest/expect': 4.1.0 + '@vitest/mocker': 4.1.0(vite@8.0.0(@types/node@25.3.5)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2)) + '@vitest/pretty-format': 4.1.0 + '@vitest/runner': 4.1.0 + '@vitest/snapshot': 4.1.0 + '@vitest/spy': 4.1.0 + '@vitest/utils': 4.1.0 + es-module-lexer: 2.0.0 expect-type: 1.3.0 magic-string: 0.30.21 obug: 2.1.1 pathe: 2.0.3 picomatch: 4.0.3 - std-env: 3.10.0 + std-env: 4.0.0 tinybench: 2.9.0 tinyexec: 1.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 - vite: 7.3.1(@types/node@25.3.5)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2) + vite: 8.0.0(@types/node@25.3.5)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 25.3.5 happy-dom: 20.8.3 transitivePeerDependencies: - - jiti - - less - - lightningcss - msw - - sass - - sass-embedded - - stylus - - sugarss - - terser - - tsx - - yaml vscode-jsonrpc@8.2.0: {} @@ -8197,15 +7459,6 @@ snapshots: transitivePeerDependencies: - supports-color - vue-loader@17.4.2(vue@3.5.29(typescript@5.9.3))(webpack@5.105.4): - dependencies: - chalk: 4.1.2 - hash-sum: 2.0.0 - watchpack: 2.5.1 - webpack: 5.105.4(webpack-cli@6.0.1) - optionalDependencies: - vue: 3.5.29(typescript@5.9.3) - vue-tsc@3.2.5(typescript@5.9.3): dependencies: '@volar/typescript': 2.4.28 @@ -8222,77 +7475,8 @@ snapshots: optionalDependencies: typescript: 5.9.3 - watchpack@2.5.1: - dependencies: - glob-to-regexp: 0.4.1 - graceful-fs: 4.2.11 - webidl-conversions@3.0.1: {} - webpack-cli@6.0.1(webpack@5.105.4): - dependencies: - '@discoveryjs/json-ext': 0.6.3 - '@webpack-cli/configtest': 3.0.1(webpack-cli@6.0.1)(webpack@5.105.4) - '@webpack-cli/info': 3.0.1(webpack-cli@6.0.1)(webpack@5.105.4) - '@webpack-cli/serve': 3.0.1(webpack-cli@6.0.1)(webpack@5.105.4) - colorette: 2.0.20 - commander: 12.1.0 - cross-spawn: 7.0.6 - envinfo: 7.21.0 - fastest-levenshtein: 1.0.16 - import-local: 3.2.0 - interpret: 3.1.1 - rechoir: 0.8.0 - webpack: 5.105.4(webpack-cli@6.0.1) - webpack-merge: 6.0.1 - - webpack-merge@6.0.1: - dependencies: - clone-deep: 4.0.1 - flat: 5.0.2 - wildcard: 2.0.1 - - webpack-sources@1.4.3: - dependencies: - source-list-map: 2.0.1 - source-map: 0.6.1 - - webpack-sources@3.3.4: {} - - webpack@5.105.4(webpack-cli@6.0.1): - dependencies: - '@types/eslint-scope': 3.7.7 - '@types/estree': 1.0.8 - '@types/json-schema': 7.0.15 - '@webassemblyjs/ast': 1.14.1 - '@webassemblyjs/wasm-edit': 1.14.1 - '@webassemblyjs/wasm-parser': 1.14.1 - acorn: 8.16.0 - acorn-import-phases: 1.0.4(acorn@8.16.0) - browserslist: 4.28.1 - chrome-trace-event: 1.0.4 - enhanced-resolve: 5.20.0 - es-module-lexer: 2.0.0 - eslint-scope: 5.1.1 - events: 3.3.0 - glob-to-regexp: 0.4.1 - graceful-fs: 4.2.11 - json-parse-even-better-errors: 2.3.1 - loader-runner: 4.3.1 - mime-types: 2.1.35 - neo-async: 2.6.2 - schema-utils: 4.3.3 - tapable: 2.3.0 - terser-webpack-plugin: 5.3.17(webpack@5.105.4) - watchpack: 2.5.1 - webpack-sources: 3.3.4 - optionalDependencies: - webpack-cli: 6.0.1(webpack@5.105.4) - transitivePeerDependencies: - - '@swc/core' - - esbuild - - uglify-js - whatwg-mimetype@3.0.0: {} whatwg-url@5.0.0: @@ -8313,8 +7497,6 @@ snapshots: siginfo: 2.0.0 stackback: 0.0.2 - wildcard@2.0.1: {} - word-wrap@1.2.5: {} wrap-ansi@10.0.0: @@ -8323,14 +7505,6 @@ snapshots: string-width: 8.2.0 strip-ansi: 7.2.0 - wrap-ansi@6.2.0: - dependencies: - ansi-styles: 4.3.0 - string-width: 4.2.3 - strip-ansi: 6.0.1 - - wrappy@1.0.2: {} - write-file-atomic@7.0.1: dependencies: signal-exit: 4.1.0 diff --git a/templates/base/head_script.tmpl b/templates/base/head_script.tmpl index b51f98de512ff..da6b82857b347 100644 --- a/templates/base/head_script.tmpl +++ b/templates/base/head_script.tmpl @@ -31,4 +31,8 @@ If you introduce mistakes in it, Gitea JavaScript code wouldn't run correctly. {{/* in case some pages don't render the pageData, we make sure it is an object to prevent null access */}} window.config.pageData = window.config.pageData || {}; - +{{/* web components must load as a blocking script to prevent flash of unstyled content */}} + +{{/* import map ensures the browser deduplicates the entry module when chunks import it without the ?v= query */}} + + diff --git a/templates/devtest/devtest-footer.tmpl b/templates/devtest/devtest-footer.tmpl index a1b3b86e5c47d..7a3b421b61181 100644 --- a/templates/devtest/devtest-footer.tmpl +++ b/templates/devtest/devtest-footer.tmpl @@ -1,3 +1,3 @@ {{/* TODO: the devtest.js is isolated from index.js, so no module is shared and many index.js functions do not work in devtest.ts */}} - + {{template "base/footer" ctx.RootData}} diff --git a/templates/repo/diff/box.tmpl b/templates/repo/diff/box.tmpl index 390e41ec340cb..690e11ff48d04 100644 --- a/templates/repo/diff/box.tmpl +++ b/templates/repo/diff/box.tmpl @@ -8,13 +8,14 @@ {{svg "octicon-sidebar-collapse" 20 "icon tw-hidden"}} {{svg "octicon-sidebar-expand" 20 "icon tw-hidden"}} - {{end}} {{if not .DiffNotAvailable}} @@ -62,9 +63,6 @@ {{if $showFileTree}} {{$.FileIconPoolHTML}}
- {{end}} {{if .DiffNotAvailable}}

{{ctx.Locale.Tr "repo.diff.data_not_available"}}

diff --git a/templates/shared/combomarkdowneditor.tmpl b/templates/shared/combomarkdowneditor.tmpl index 55da6db9b48ac..2c394a6dc11f4 100644 --- a/templates/shared/combomarkdowneditor.tmpl +++ b/templates/shared/combomarkdowneditor.tmpl @@ -75,7 +75,7 @@ {{if .DisableAutosize}}data-disable-autosize="{{.DisableAutosize}}"{{end}} >{{.TextareaContent}} - + diff --git a/tests/integration/markup_external_test.go b/tests/integration/markup_external_test.go index 691ffcc62b504..b6932258a6608 100644 --- a/tests/integration/markup_external_test.go +++ b/tests/integration/markup_external_test.go @@ -107,7 +107,7 @@ func TestExternalMarkupRenderer(t *testing.T) { // default sandbox in sub page response assert.Equal(t, "frame-src 'self'; sandbox allow-scripts allow-popups", respSub.Header().Get("Content-Security-Policy")) // FIXME: actually here is a bug (legacy design problem), the "PostProcess" will escape "
<script></script>
`, respSub.Body.String()) + assert.Equal(t, `
<script></script>
`, respSub.Body.String()) }) }) diff --git a/tsconfig.json b/tsconfig.json index 9b978cf54eaa3..851bf13dc9c73 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -45,7 +45,7 @@ "verbatimModuleSyntax": true, "types": [ "node", - "webpack/module", + "vite/client", "vitest/globals", "./web_src/js/globals.d.ts", "./types.d.ts", diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000000000..1c7167f5af222 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,208 @@ +import {build, defineConfig, type Plugin} from 'vite'; +import vuePlugin from '@vitejs/plugin-vue'; +import {stringPlugin} from 'vite-string-plugin'; +import {readFileSync, globSync} from 'node:fs'; +import {fileURLToPath} from 'node:url'; +import {parse} from 'node:path'; +import {env} from 'node:process'; +import tailwindcss from 'tailwindcss'; +import tailwindConfig from './tailwind.config.ts'; +import wrapAnsi from 'wrap-ansi'; +import licensePlugin from 'rollup-plugin-license'; + +const isProduction = env.NODE_ENV !== 'development'; + +let enableSourcemap: boolean; +if ('ENABLE_SOURCEMAP' in env) { + enableSourcemap = env.ENABLE_SOURCEMAP !== 'false'; +} else { + enableSourcemap = !isProduction; +} + +const outDir = fileURLToPath(new URL('public/assets', import.meta.url)); +const buildTarget = 'es2020'; + +const themes: Record = {}; +for (const path of globSync('web_src/css/themes/*.css', {cwd: import.meta.dirname})) { + themes[parse(path).name] = fileURLToPath(new URL(path, import.meta.url)); +} + +const webComponents = new Set([ + // our own, in web_src/js/webcomponents + 'overflow-menu', + 'origin-url', + // from dependencies + 'markdown-toolbar', + 'relative-time', + 'text-expander', +]); + +const formatLicenseText = (licenseText: string) => wrapAnsi(licenseText || '', 80).trim(); + +// Build web components as a separate IIFE bundle that loads as a blocking script +// to prevent flash of unstyled content. This runs as part of the main build. +function webcomponentsPlugin(): Plugin { + return { + name: 'webcomponents-iife', + async closeBundle() { + await build({ + configFile: false, + root: import.meta.dirname, + publicDir: false, + build: { + outDir, + emptyOutDir: false, + sourcemap: enableSourcemap, + target: buildTarget, + minify: isProduction, + reportCompressedSize: false, + lib: { + entry: fileURLToPath(new URL('web_src/js/webcomponents/webcomponents-blocking.ts', import.meta.url)), + formats: ['iife'], + name: 'webcomponents', + }, + rolldownOptions: { + output: { + entryFileNames: 'js/webcomponents.js', + }, + }, + }, + define: { + 'process.env.NODE_ENV': JSON.stringify(isProduction ? 'production' : 'development'), // for tippy.js + }, + plugins: [ + stringPlugin(), + ], + }); + }, + }; +} + +// Filter out legacy font formats from CSS, keeping only woff2 +function filterCssUrlPlugin(): Plugin { + return { + name: 'filter-css-url', + enforce: 'pre', + transform(code, id) { + if (!id.endsWith('.css') || !id.includes('katex')) return null; + code = code.replace(/,\s*url\([^)]*\.woff\)\s*format\("[^"]*"\)/gi, ''); + code = code.replace(/,\s*url\([^)]*\.ttf\)\s*format\("[^"]*"\)/gi, ''); + return code; + }, + }; +} + +export default defineConfig({ + root: import.meta.dirname, + base: './', + publicDir: false, + build: { + outDir, + emptyOutDir: false, + modulePreload: false, + sourcemap: enableSourcemap, + target: buildTarget, + minify: isProduction, + chunkSizeWarningLimit: Infinity, + reportCompressedSize: false, + rolldownOptions: { + checks: { + eval: false, // htmx needs eval + pluginTimings: false, + }, + input: { + index: fileURLToPath(new URL('web_src/js/index.ts', import.meta.url)), + swagger: fileURLToPath(new URL('web_src/js/standalone/swagger.ts', import.meta.url)), + 'external-render-iframe': fileURLToPath(new URL('web_src/js/standalone/external-render-iframe.ts', import.meta.url)), + 'eventsource.sharedworker': fileURLToPath(new URL('web_src/js/features/eventsource.sharedworker.ts', import.meta.url)), + ...(!isProduction && { + devtest: fileURLToPath(new URL('web_src/js/standalone/devtest.ts', import.meta.url)), + }), + ...themes, + }, + output: { + entryFileNames: 'js/[name].js', + chunkFileNames: 'js/[name].[hash:8].js', + assetFileNames: (info: {name?: string}) => { + const name = (info.name ?? '').split('?')[0]; + if (/\.css$/i.test(name)) { + // css entrypoints with no hash, cache-busted via ?v= in templates + if (/^(index|swagger|external-render-iframe|devtest|theme-.*)\.css$/i.test(name)) { + return `css/${name}`; + } + return 'css/[name].[hash:8].css'; + } + if (/\.(ttf|woff2?)$/i.test(name)) return 'fonts/[name].[hash:8].[ext]'; + return '[name].[hash:8].[ext]'; + }, + }, + }, + }, + css: { + transformer: 'postcss', + postcss: { + plugins: [ + tailwindcss(tailwindConfig), + ], + }, + }, + experimental: { + renderBuiltUrl(filename, {hostType}) { + if (hostType === 'css') { + return `../${filename}`; // CSS files are in css/, assets are siblings, so go up one level + } + return {relative: true}; + }, + }, + define: { + __VUE_OPTIONS_API__: true, + __VUE_PROD_DEVTOOLS__: false, + __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: false, + }, + worker: { + rolldownOptions: { + output: { + entryFileNames: 'js/[name].[hash:8].js', + }, + }, + }, + plugins: [ + webcomponentsPlugin(), + filterCssUrlPlugin(), + stringPlugin(), + vuePlugin({ + template: { + compilerOptions: { + isCustomElement: (tag: string) => webComponents.has(tag), + }, + }, + }), + isProduction && licensePlugin({ + thirdParty: { + output: { + file: fileURLToPath(new URL('public/assets/licenses.txt', import.meta.url)), + template(dependencies) { + const line = '-'.repeat(80); + const goJson = readFileSync('assets/go-licenses.json', 'utf8'); + const goModules = JSON.parse(goJson).map(({name, licenseText}: Record) => { + return {name, body: formatLicenseText(licenseText)}; + }); + const jsModules = dependencies.map((dep) => { + return {name: dep.name, version: dep.version, body: formatLicenseText(dep.licenseText ?? '')}; + }); + + const modules = [...goModules, ...jsModules].sort((a, b) => a.name.localeCompare(b.name)); + return modules.map(({name, version, body}: Record) => { + const title = version ? `${name}@${version}` : name; + return `${line}\n${title}\n${line}\n${body}`; + }).join('\n'); + }, + }, + allow(dependency) { + if (dependency.name === 'khroma') return true; // MIT: https://github.com/fabiospampinato/khroma/pull/33 + return /(Apache-2\.0|0BSD|BSD-2-Clause|BSD-3-Clause|MIT|ISC|CPAL-1\.0|Unlicense|EPL-1\.0|EPL-2\.0)/.test(dependency.license ?? ''); + }, + }, + }), + ], +}); diff --git a/web_src/js/bootstrap.ts b/web_src/js/bootstrap.ts index ca38ac874e18c..83f8383fe8e71 100644 --- a/web_src/js/bootstrap.ts +++ b/web_src/js/bootstrap.ts @@ -4,16 +4,14 @@ import type {Intent} from './types.ts'; import {html} from './utils/html.ts'; -// This sets up the URL prefix used in webpack's chunk loading. // This file must be imported before any lazy-loading is being attempted. -window.__webpack_public_path__ = `${window.config?.assetUrlPrefix ?? '/assets'}/`; export function shouldIgnoreError(err: Error) { const ignorePatterns: Array = [ // https://github.com/go-gitea/gitea/issues/30861 // https://github.com/microsoft/monaco-editor/issues/4496 // https://github.com/microsoft/monaco-editor/issues/4679 - /\/assets\/js\/.*monaco/, + /\/assets\/js\/.*(monaco|editor\.(api|worker))/, ]; for (const pattern of ignorePatterns) { if (pattern.test(err.stack ?? '')) return true; @@ -44,7 +42,7 @@ export function showGlobalErrorMessage(msg: string, msgType: Intent = 'error') { function processWindowErrorEvent({error, reason, message, type, filename, lineno, colno}: ErrorEvent & PromiseRejectionEvent) { const err = error ?? reason; - const assetBaseUrl = String(new URL(window.__webpack_public_path__, window.location.origin)); + const assetBaseUrl = String(new URL(`${window.config?.assetUrlPrefix ?? '/assets'}/`, window.location.origin)); const {runModeIsProd} = window.config ?? {}; // `error` and `reason` are not guaranteed to be errors. If the value is falsy, it is likely a diff --git a/web_src/js/features/captcha.ts b/web_src/js/features/captcha.ts index 01b50530265f4..08513fe6ba5a1 100644 --- a/web_src/js/features/captcha.ts +++ b/web_src/js/features/captcha.ts @@ -34,7 +34,7 @@ export async function initCaptcha() { break; } case 'm-captcha': { - const mCaptcha = await import(/* webpackChunkName: "mcaptcha-vanilla-glue" */'@mcaptcha/vanilla-glue'); + const mCaptcha = await import('@mcaptcha/vanilla-glue'); // FIXME: the mCaptcha code is not right, it's a miracle that the wrong code could run // * the "vanilla-glue" has some problems with es6 module. diff --git a/web_src/js/features/citation.ts b/web_src/js/features/citation.ts index 79d932eab416d..92bdac4f90828 100644 --- a/web_src/js/features/citation.ts +++ b/web_src/js/features/citation.ts @@ -6,10 +6,10 @@ const {pageData} = window.config; async function initInputCitationValue(citationCopyApa: HTMLButtonElement, citationCopyBibtex: HTMLButtonElement) { const [{Cite, plugins}] = await Promise.all([ - import(/* webpackChunkName: "citation-js-core" */'@citation-js/core'), - import(/* webpackChunkName: "citation-js-formats" */'@citation-js/plugin-software-formats'), - import(/* webpackChunkName: "citation-js-bibtex" */'@citation-js/plugin-bibtex'), - import(/* webpackChunkName: "citation-js-csl" */'@citation-js/plugin-csl'), + import('@citation-js/core'), + import('@citation-js/plugin-software-formats'), + import('@citation-js/plugin-bibtex'), + import('@citation-js/plugin-csl'), ]); const {citationFileContent} = pageData; const config = plugins.config.get('@bibtex'); diff --git a/web_src/js/features/code-frequency.ts b/web_src/js/features/code-frequency.ts index da7cd6b2c0094..475379ac14a6d 100644 --- a/web_src/js/features/code-frequency.ts +++ b/web_src/js/features/code-frequency.ts @@ -4,7 +4,7 @@ export async function initRepoCodeFrequency() { const el = document.querySelector('#repo-code-frequency-chart'); if (!el) return; - const {default: RepoCodeFrequency} = await import(/* webpackChunkName: "code-frequency-graph" */'../components/RepoCodeFrequency.vue'); + const {default: RepoCodeFrequency} = await import('../components/RepoCodeFrequency.vue'); try { const View = createApp(RepoCodeFrequency, { locale: { diff --git a/web_src/js/features/codeeditor.ts b/web_src/js/features/codeeditor.ts index dc3f2fad81bb4..58acf1494d53c 100644 --- a/web_src/js/features/codeeditor.ts +++ b/web_src/js/features/codeeditor.ts @@ -129,7 +129,7 @@ function updateTheme(monaco: Monaco): void { type CreateMonacoOpts = MonacoOpts & {language?: string}; export async function createMonaco(textarea: HTMLTextAreaElement, filename: string, opts: CreateMonacoOpts): Promise<{monaco: Monaco, editor: IStandaloneCodeEditor}> { - const monaco = await import(/* webpackChunkName: "monaco" */'monaco-editor'); + const monaco = await import('../modules/monaco.ts'); initLanguages(monaco); let {language, ...other} = opts; diff --git a/web_src/js/features/colorpicker.ts b/web_src/js/features/colorpicker.ts index face4ef228f4f..6a14774bfc4f2 100644 --- a/web_src/js/features/colorpicker.ts +++ b/web_src/js/features/colorpicker.ts @@ -6,8 +6,8 @@ export async function initColorPickers() { registerGlobalInitFunc('initColorPicker', async (el) => { if (!imported) { await Promise.all([ - import(/* webpackChunkName: "colorpicker" */'vanilla-colorful/hex-color-picker.js'), - import(/* webpackChunkName: "colorpicker" */'../../css/features/colorpicker.css'), + import('vanilla-colorful/hex-color-picker.js'), + import('../../css/features/colorpicker.css'), ]); imported = true; } diff --git a/web_src/js/features/comp/ComboMarkdownEditor.ts b/web_src/js/features/comp/ComboMarkdownEditor.ts index 5b470ea03d563..468f3fc5ca6db 100644 --- a/web_src/js/features/comp/ComboMarkdownEditor.ts +++ b/web_src/js/features/comp/ComboMarkdownEditor.ts @@ -319,8 +319,8 @@ export class ComboMarkdownEditor { async switchToEasyMDE() { if (this.easyMDE) return; const [{default: EasyMDE}] = await Promise.all([ - import(/* webpackChunkName: "easymde" */'easymde'), - import(/* webpackChunkName: "easymde" */'../../../css/easymde.css'), + import('easymde'), + import('../../../css/easymde.css'), ]); const easyMDEOpt: EasyMDE.Options = { autoDownloadFontAwesome: false, diff --git a/web_src/js/features/comp/Cropper.ts b/web_src/js/features/comp/Cropper.ts index 9fd48697fa6af..a36689bfc2718 100644 --- a/web_src/js/features/comp/Cropper.ts +++ b/web_src/js/features/comp/Cropper.ts @@ -7,7 +7,7 @@ type CropperOpts = { }; async function initCompCropper({container, fileInput, imageSource}: CropperOpts) { - const {default: Cropper} = await import(/* webpackChunkName: "cropperjs" */'cropperjs'); + const {default: Cropper} = await import('cropperjs'); let currentFileName = ''; let currentFileLastModified = 0; const cropper = new Cropper(imageSource, { diff --git a/web_src/js/features/contributors.ts b/web_src/js/features/contributors.ts index 95fc81f5b368b..d28d18eacc23f 100644 --- a/web_src/js/features/contributors.ts +++ b/web_src/js/features/contributors.ts @@ -4,7 +4,7 @@ export async function initRepoContributors() { const el = document.querySelector('#repo-contributors-chart'); if (!el) return; - const {default: RepoContributors} = await import(/* webpackChunkName: "contributors-graph" */'../components/RepoContributors.vue'); + const {default: RepoContributors} = await import('../components/RepoContributors.vue'); try { const View = createApp(RepoContributors, { repoLink: el.getAttribute('data-repo-link'), diff --git a/web_src/js/features/dropzone.ts b/web_src/js/features/dropzone.ts index fedcff2162b54..55c0e3c7a5096 100644 --- a/web_src/js/features/dropzone.ts +++ b/web_src/js/features/dropzone.ts @@ -19,8 +19,8 @@ export const DropzoneCustomEventUploadDone = 'dropzone-custom-upload-done'; async function createDropzone(el: HTMLElement, opts: DropzoneOptions) { const [{default: Dropzone}] = await Promise.all([ - import(/* webpackChunkName: "dropzone" */'dropzone'), - import(/* webpackChunkName: "dropzone" */'dropzone/dist/dropzone.css'), + import('dropzone'), + import('dropzone/dist/dropzone.css'), ]); return new Dropzone(el, opts); } diff --git a/web_src/js/features/heatmap.ts b/web_src/js/features/heatmap.ts index 95004096d8a9a..341e014bfc6c9 100644 --- a/web_src/js/features/heatmap.ts +++ b/web_src/js/features/heatmap.ts @@ -45,7 +45,7 @@ export async function initHeatmap() { noDataText: el.getAttribute('data-locale-no-contributions'), }; - const {default: ActivityHeatmap} = await import(/* webpackChunkName: "ActivityHeatmap" */ '../components/ActivityHeatmap.vue'); + const {default: ActivityHeatmap} = await import('../components/ActivityHeatmap.vue'); const View = createApp(ActivityHeatmap, {values, locale}); View.mount(el); el.classList.remove('is-loading'); diff --git a/web_src/js/features/notification.ts b/web_src/js/features/notification.ts index 915f65f88d8b2..e5246d19b14de 100644 --- a/web_src/js/features/notification.ts +++ b/web_src/js/features/notification.ts @@ -2,7 +2,7 @@ import {GET} from '../modules/fetch.ts'; import {toggleElem, createElementFromHTML} from '../utils/dom.ts'; import {logoutFromWorker} from '../modules/worker.ts'; -const {appSubUrl, notificationSettings, assetVersionEncoded} = window.config; +const {appSubUrl, notificationSettings, assetVersionEncoded, assetUrlPrefix} = window.config; let notificationSequenceNumber = 0; async function receiveUpdateCount(event: MessageEvent<{type: string, data: string}>) { @@ -33,7 +33,7 @@ export function initNotificationCount() { if (notificationSettings.EventSourceUpdateTime > 0 && window.EventSource && window.SharedWorker) { // Try to connect to the event source via the shared worker first - const worker = new SharedWorker(`${window.__webpack_public_path__}js/eventsource.sharedworker.js?v=${assetVersionEncoded}`, 'notification-worker'); + const worker = new SharedWorker(`${assetUrlPrefix}/js/eventsource.sharedworker.js?v=${assetVersionEncoded}`, 'notification-worker'); worker.addEventListener('error', (event) => { console.error('worker error', event); }); diff --git a/web_src/js/features/recent-commits.ts b/web_src/js/features/recent-commits.ts index b7f7c4998736d..6ad53a238c0ce 100644 --- a/web_src/js/features/recent-commits.ts +++ b/web_src/js/features/recent-commits.ts @@ -4,7 +4,7 @@ export async function initRepoRecentCommits() { const el = document.querySelector('#repo-recent-commits-chart'); if (!el) return; - const {default: RepoRecentCommits} = await import(/* webpackChunkName: "recent-commits-graph" */'../components/RepoRecentCommits.vue'); + const {default: RepoRecentCommits} = await import('../components/RepoRecentCommits.vue'); try { const View = createApp(RepoRecentCommits, { locale: { diff --git a/web_src/js/features/repo-findfile.ts b/web_src/js/features/repo-findfile.ts index 8d306b2bab8f1..962f8b84c1254 100644 --- a/web_src/js/features/repo-findfile.ts +++ b/web_src/js/features/repo-findfile.ts @@ -69,7 +69,7 @@ export function filterRepoFilesWeighted(files: Array, filter: string) { export function initRepoFileSearch() { registerGlobalInitFunc('initRepoFileSearch', async (el) => { - const {default: RepoFileSearch} = await import(/* webpackChunkName: "RepoFileSearch" */ '../components/RepoFileSearch.vue'); + const {default: RepoFileSearch} = await import('../components/RepoFileSearch.vue'); createApp(RepoFileSearch, { repoLink: el.getAttribute('data-repo-link'), currentRefNameSubURL: el.getAttribute('data-current-ref-name-sub-url'), diff --git a/web_src/js/features/repo-issue-pull.ts b/web_src/js/features/repo-issue-pull.ts index 093f484b42caa..58dbf1790ebe4 100644 --- a/web_src/js/features/repo-issue-pull.ts +++ b/web_src/js/features/repo-issue-pull.ts @@ -66,7 +66,7 @@ async function initRepoPullRequestMergeForm(box: HTMLElement) { const el = box.querySelector('#pull-request-merge-form'); if (!el) return; - const {default: PullRequestMergeForm} = await import(/* webpackChunkName: "PullRequestMergeForm" */ '../components/PullRequestMergeForm.vue'); + const {default: PullRequestMergeForm} = await import('../components/PullRequestMergeForm.vue'); const view = createApp(PullRequestMergeForm); view.mount(el); } diff --git a/web_src/js/features/stopwatch.ts b/web_src/js/features/stopwatch.ts index 34e985332b3a6..ee8b49d2e1f3d 100644 --- a/web_src/js/features/stopwatch.ts +++ b/web_src/js/features/stopwatch.ts @@ -3,7 +3,7 @@ import {GET} from '../modules/fetch.ts'; import {hideElem, queryElems, showElem} from '../utils/dom.ts'; import {logoutFromWorker} from '../modules/worker.ts'; -const {appSubUrl, notificationSettings, enableTimeTracking, assetVersionEncoded} = window.config; +const {appSubUrl, notificationSettings, enableTimeTracking, assetVersionEncoded, assetUrlPrefix} = window.config; export function initStopwatch() { if (!enableTimeTracking) { @@ -47,7 +47,7 @@ export function initStopwatch() { // if the browser supports EventSource and SharedWorker, use it instead of the periodic poller if (notificationSettings.EventSourceUpdateTime > 0 && window.EventSource && window.SharedWorker) { // Try to connect to the event source via the shared worker first - const worker = new SharedWorker(`${window.__webpack_public_path__}js/eventsource.sharedworker.js?v=${assetVersionEncoded}`, 'notification-worker'); + const worker = new SharedWorker(`${assetUrlPrefix}/js/eventsource.sharedworker.js?v=${assetVersionEncoded}`, 'notification-worker'); worker.addEventListener('error', (event) => { console.error('worker error', event); }); diff --git a/web_src/js/features/tribute.ts b/web_src/js/features/tribute.ts index 1a011c33a191b..462a925ab67c8 100644 --- a/web_src/js/features/tribute.ts +++ b/web_src/js/features/tribute.ts @@ -5,7 +5,7 @@ import type {TributeCollection} from 'tributejs'; import type {Mention} from '../types.ts'; export async function attachTribute(element: HTMLElement) { - const {default: Tribute} = await import(/* webpackChunkName: "tribute" */'tributejs'); + const {default: Tribute} = await import('tributejs'); const mentionsUrl = element.closest('[data-mentions-url]')?.getAttribute('data-mentions-url'); const emojiCollection: TributeCollection = { // emojis diff --git a/web_src/js/globals.d.ts b/web_src/js/globals.d.ts index ff025efbf57a3..688d141deab16 100644 --- a/web_src/js/globals.d.ts +++ b/web_src/js/globals.d.ts @@ -42,6 +42,10 @@ interface Window { codeEditors: any[], // export editor for customization localUserSettings: typeof import('./modules/user-settings.ts').localUserSettings, + MonacoEnvironment?: { + getWorker: (workerId: string, label: string) => Worker, + }, + // various captcha plugins grecaptcha: any, turnstile: any, @@ -49,3 +53,8 @@ interface Window { // do not add more properties here unless it is a must } + +declare module '*?worker' { + const workerConstructor: new () => Worker; + export default workerConstructor; +} diff --git a/web_src/js/index.ts b/web_src/js/index.ts index 2de29f52b947d..e224aa7f2f106 100644 --- a/web_src/js/index.ts +++ b/web_src/js/index.ts @@ -1,11 +1,13 @@ -// bootstrap module must be the first one to be imported, it handles webpack lazy-loading and global errors +// bootstrap module must be the first one to be imported, it handles global errors import './bootstrap.ts'; +import '../fomantic/build/fomantic.css'; +import '../css/index.css'; + // many users expect to use jQuery in their custom scripts (https://docs.gitea.com/administration/customizing-gitea#example-plantuml) // so load globals (including jQuery) as early as possible import './globals.ts'; -import './webcomponents/index.ts'; import './modules/user-settings.ts'; // templates also need to use localUserSettings in inline scripts import {onDomReady} from './utils/dom.ts'; @@ -15,12 +17,11 @@ import {onDomReady} from './utils/dom.ts'; import 'htmx.org'; onDomReady(async () => { - // when navigate before the import complete, there will be an error from webpack chunk loader: - // JavaScript promise rejection: Loading chunk index-domready failed. try { - await import(/* webpackChunkName: "index-domready" */'./index-domready.ts'); + await import('./index-domready.ts'); } catch (e) { - if (e.name === 'ChunkLoadError') { + // When navigating away before the import completes, a TypeError is thrown + if (e instanceof TypeError) { console.error('Error loading index-domready:', e); } else { throw e; diff --git a/web_src/js/markup/asciicast.ts b/web_src/js/markup/asciicast.ts index 4596327876c5d..90515e1363cdb 100644 --- a/web_src/js/markup/asciicast.ts +++ b/web_src/js/markup/asciicast.ts @@ -3,8 +3,8 @@ import {queryElems} from '../utils/dom.ts'; export async function initMarkupRenderAsciicast(elMarkup: HTMLElement): Promise { queryElems(elMarkup, '.asciinema-player-container', async (el) => { const [player] = await Promise.all([ - import(/* webpackChunkName: "asciinema-player" */'asciinema-player'), - import(/* webpackChunkName: "asciinema-player" */'asciinema-player/dist/bundle/asciinema-player.css'), + import('asciinema-player'), + import('asciinema-player/dist/bundle/asciinema-player.css'), ]); player.create(el.getAttribute('data-asciinema-player-src')!, el, { diff --git a/web_src/js/markup/math.ts b/web_src/js/markup/math.ts index bc118137a1093..a3ee102ccdeb7 100644 --- a/web_src/js/markup/math.ts +++ b/web_src/js/markup/math.ts @@ -16,8 +16,8 @@ export async function initMarkupCodeMath(elMarkup: HTMLElement): Promise { // .markup code.language-math' queryElems(elMarkup, 'code.language-math', async (el) => { const [{default: katex}] = await Promise.all([ - import(/* webpackChunkName: "katex" */'katex'), - import(/* webpackChunkName: "katex" */'katex/dist/katex.css'), + import('katex'), + import('katex/dist/katex.css'), ]); const MAX_CHARS = 1000; diff --git a/web_src/js/markup/mermaid.ts b/web_src/js/markup/mermaid.ts index 5148ff377ca41..aaf6da6805db2 100644 --- a/web_src/js/markup/mermaid.ts +++ b/web_src/js/markup/mermaid.ts @@ -72,8 +72,8 @@ export function sourceNeedsElk(source: string) { } async function loadMermaid(needElkRender: boolean) { - const mermaidPromise = import(/* webpackChunkName: "mermaid" */'mermaid'); - const elkPromise = needElkRender ? import(/* webpackChunkName: "mermaid-layout-elk" */'@mermaid-js/layout-elk') : null; + const mermaidPromise = import('mermaid'); + const elkPromise = needElkRender ? import('@mermaid-js/layout-elk') : null; const results = await Promise.all([mermaidPromise, elkPromise]); return { mermaid: results[0].default, diff --git a/web_src/js/markup/refissue.ts b/web_src/js/markup/refissue.ts index f2fcd24f39de3..b17f452dd4d4b 100644 --- a/web_src/js/markup/refissue.ts +++ b/web_src/js/markup/refissue.ts @@ -20,7 +20,7 @@ function showMarkupRefIssuePopup(e: MouseEvent | FocusEvent) { const el = document.createElement('div'); const onShowAsync = async () => { - const {default: ContextPopup} = await import(/* webpackChunkName: "ContextPopup" */ '../components/ContextPopup.vue'); + const {default: ContextPopup} = await import('../components/ContextPopup.vue'); const view = createApp(ContextPopup, { // backend: GetIssueInfo loadIssueInfoUrl: `${window.config.appSubUrl}/${issuePathInfo.ownerName}/${issuePathInfo.repoName}/issues/${issuePathInfo.indexString}/info`, diff --git a/web_src/js/modules/monaco.ts b/web_src/js/modules/monaco.ts new file mode 100644 index 0000000000000..c8e1ff77655de --- /dev/null +++ b/web_src/js/modules/monaco.ts @@ -0,0 +1,17 @@ +import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'; +import jsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker'; +import cssWorker from 'monaco-editor/esm/vs/language/css/css.worker?worker'; +import htmlWorker from 'monaco-editor/esm/vs/language/html/html.worker?worker'; +import tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker'; + +window.MonacoEnvironment = { + getWorker(_: string, label: string) { + if (label === 'json') return new jsonWorker(); + if (label === 'css' || label === 'scss' || label === 'less') return new cssWorker(); + if (label === 'html' || label === 'handlebars' || label === 'razor') return new htmlWorker(); + if (label === 'typescript' || label === 'javascript') return new tsWorker(); + return new editorWorker(); + }, +}; + +export * from 'monaco-editor'; diff --git a/web_src/js/modules/sortable.ts b/web_src/js/modules/sortable.ts index f3515fcb8de19..c49f36ba8beee 100644 --- a/web_src/js/modules/sortable.ts +++ b/web_src/js/modules/sortable.ts @@ -3,7 +3,7 @@ import type SortableType from 'sortablejs'; export async function createSortable(el: HTMLElement, opts: {handle?: string} & SortableOptions = {}): Promise { // type reassigned because typescript derives the wrong type from this import - const {Sortable} = (await import(/* webpackChunkName: "sortablejs" */'sortablejs') as unknown as {Sortable: typeof SortableType}); + const {Sortable} = (await import('sortablejs') as unknown as {Sortable: typeof SortableType}); return new Sortable(el, { animation: 150, diff --git a/web_src/js/render/plugins/3d-viewer.ts b/web_src/js/render/plugins/3d-viewer.ts index 6f3ee15d2653e..f997790af696c 100644 --- a/web_src/js/render/plugins/3d-viewer.ts +++ b/web_src/js/render/plugins/3d-viewer.ts @@ -47,7 +47,7 @@ export function newRenderPlugin3DViewer(): FileRenderPlugin { async render(container: HTMLElement, fileUrl: string): Promise { // TODO: height and/or max-height? - const OV = await import(/* webpackChunkName: "online-3d-viewer" */'online-3d-viewer'); + const OV = await import('online-3d-viewer'); const viewer = new OV.EmbeddedViewer(container, { backgroundColor: new OV.RGBAColor(59, 68, 76, 0), defaultColor: new OV.RGBColor(65, 131, 196), diff --git a/web_src/js/render/plugins/pdf-viewer.ts b/web_src/js/render/plugins/pdf-viewer.ts index 40623be05576f..c7040e96ef13f 100644 --- a/web_src/js/render/plugins/pdf-viewer.ts +++ b/web_src/js/render/plugins/pdf-viewer.ts @@ -9,7 +9,7 @@ export function newRenderPluginPdfViewer(): FileRenderPlugin { }, async render(container: HTMLElement, fileUrl: string): Promise { - const PDFObject = await import(/* webpackChunkName: "pdfobject" */'pdfobject'); + const PDFObject = await import('pdfobject'); // TODO: the PDFObject library does not support dynamic height adjustment, container.style.height = `${window.innerHeight - 100}px`; if (!PDFObject.default.embed(fileUrl, container)) { diff --git a/web_src/js/standalone/devtest.ts b/web_src/js/standalone/devtest.ts index 39c41db042465..20ab163d1a27b 100644 --- a/web_src/js/standalone/devtest.ts +++ b/web_src/js/standalone/devtest.ts @@ -1,3 +1,4 @@ +import '../../css/standalone/devtest.css'; import {showInfoToast, showWarningToast, showErrorToast, type Toast} from '../modules/toast.ts'; type LevelMap = Record Toast | null>; diff --git a/web_src/js/standalone/external-render-iframe.ts b/web_src/js/standalone/external-render-iframe.ts index f8ec070785aaa..3b489f8ee382b 100644 --- a/web_src/js/standalone/external-render-iframe.ts +++ b/web_src/js/standalone/external-render-iframe.ts @@ -11,6 +11,8 @@ RENDER_COMMAND = `echo '
foobar
'); expect(querySingleVisibleElem(el, 'span')!.textContent).toEqual('bar'); el = createElementFromHTML('
foobar
'); - expect(() => querySingleVisibleElem(el, 'span')).toThrowError('Expected exactly one visible element'); + expect(() => querySingleVisibleElem(el, 'span')).toThrow('Expected exactly one visible element'); }); test('queryElemChildren', () => { diff --git a/web_src/js/utils/testhelper.ts b/web_src/js/utils/testhelper.ts index 59eb39778c232..9541ecdefb2e4 100644 --- a/web_src/js/utils/testhelper.ts +++ b/web_src/js/utils/testhelper.ts @@ -2,7 +2,7 @@ // even if backend is in testing mode, frontend could be complied in production mode // so this function only checks if the frontend is in unit testing mode (usually from *.test.ts files) export function isInFrontendUnitTest() { - return import.meta.env.TEST === 'true'; + return import.meta.env.MODE === 'test'; } /** strip common indentation from a string and trim it */ diff --git a/web_src/js/vitest.setup.ts b/web_src/js/vitest.setup.ts index ff8efb697accb..cc363f3490036 100644 --- a/web_src/js/vitest.setup.ts +++ b/web_src/js/vitest.setup.ts @@ -1,5 +1,3 @@ -window.__webpack_public_path__ = ''; - window.config = { appUrl: 'http://localhost:3000/', appSubUrl: '', diff --git a/web_src/js/webcomponents/webcomponents-blocking.ts b/web_src/js/webcomponents/webcomponents-blocking.ts new file mode 100644 index 0000000000000..f2d725814d77b --- /dev/null +++ b/web_src/js/webcomponents/webcomponents-blocking.ts @@ -0,0 +1,4 @@ +import './polyfills.ts'; +import './relative-time.ts'; +import './origin-url.ts'; +import './overflow-menu.ts'; diff --git a/webpack.config.ts b/webpack.config.ts deleted file mode 100644 index ef082393540e0..0000000000000 --- a/webpack.config.ts +++ /dev/null @@ -1,269 +0,0 @@ -import wrapAnsi from 'wrap-ansi'; -import AddAssetPlugin from 'add-asset-webpack-plugin'; -import LicenseCheckerWebpackPlugin from '@techknowlogick/license-checker-webpack-plugin'; -import MiniCssExtractPlugin from 'mini-css-extract-plugin'; -import MonacoWebpackPlugin from 'monaco-editor-webpack-plugin'; -import {VueLoaderPlugin} from 'vue-loader'; -import {EsbuildPlugin} from 'esbuild-loader'; -import {parse} from 'node:path'; -import webpack, {type Configuration, type EntryObject} from 'webpack'; -import {fileURLToPath} from 'node:url'; -import {readFileSync, globSync} from 'node:fs'; -import {env} from 'node:process'; -import tailwindcss from 'tailwindcss'; -import tailwindConfig from './tailwind.config.ts'; - -const {SourceMapDevToolPlugin, DefinePlugin, EnvironmentPlugin} = webpack; -const formatLicenseText = (licenseText: string) => wrapAnsi(licenseText || '', 80).trim(); - -const themes: EntryObject = {}; -for (const path of globSync('web_src/css/themes/*.css', {cwd: import.meta.dirname})) { - themes[parse(path).name] = [`./${path}`]; -} - -const isProduction = env.NODE_ENV !== 'development'; - -// ENABLE_SOURCEMAP accepts the following values: -// true - all enabled, the default in development -// reduced - minimal sourcemaps, the default in production -// false - all disabled -let sourceMaps; -if ('ENABLE_SOURCEMAP' in env) { - sourceMaps = ['true', 'false'].includes(env.ENABLE_SOURCEMAP || '') ? env.ENABLE_SOURCEMAP : 'reduced'; -} else { - sourceMaps = isProduction ? 'reduced' : 'true'; -} - -// define which web components we use for Vue to not interpret them as Vue components -const webComponents = new Set([ - // our own, in web_src/js/webcomponents - 'overflow-menu', - 'origin-url', - // from dependencies - 'markdown-toolbar', - 'relative-time', - 'text-expander', -]); - -const filterCssImport = (url: string, ...args: Array) => { - const cssFile = args[1] || args[0]; // resourcePath is 2nd argument for url and 3rd for import - const importedFile = url.replace(/[?#].+/, '').toLowerCase(); - - if (cssFile.includes('fomantic')) { - if (importedFile.includes('brand-icons')) return false; - if (/(eot|ttf|otf|woff|svg)$/i.test(importedFile)) return false; - } - - if (cssFile.includes('katex') && /(ttf|woff)$/i.test(importedFile)) { - return false; - } - - return true; -}; - -export default { - mode: isProduction ? 'production' : 'development', - entry: { - index: [ - fileURLToPath(new URL('web_src/js/index.ts', import.meta.url)), - fileURLToPath(new URL('web_src/fomantic/build/fomantic.css', import.meta.url)), - fileURLToPath(new URL('web_src/css/index.css', import.meta.url)), - ], - swagger: [ - fileURLToPath(new URL('web_src/js/standalone/swagger.ts', import.meta.url)), - fileURLToPath(new URL('web_src/css/standalone/swagger.css', import.meta.url)), - ], - 'external-render-iframe': [ - fileURLToPath(new URL('web_src/js/standalone/external-render-iframe.ts', import.meta.url)), - fileURLToPath(new URL('web_src/css/standalone/external-render-iframe.css', import.meta.url)), - ], - 'eventsource.sharedworker': [ - fileURLToPath(new URL('web_src/js/features/eventsource.sharedworker.ts', import.meta.url)), - ], - ...(!isProduction && { - devtest: [ - fileURLToPath(new URL('web_src/js/standalone/devtest.ts', import.meta.url)), - fileURLToPath(new URL('web_src/css/standalone/devtest.css', import.meta.url)), - ], - }), - ...themes, - }, - devtool: false, - output: { - path: fileURLToPath(new URL('public/assets', import.meta.url)), - filename: 'js/[name].js', - chunkFilename: 'js/[name].[contenthash:8].js', - }, - optimization: { - minimize: isProduction, - minimizer: [ - new EsbuildPlugin({ - target: 'es2020', - minify: true, - css: true, - legalComments: 'none', - }), - ], - moduleIds: 'named', - chunkIds: 'named', - }, - module: { - rules: [ - { - test: /\.vue$/i, - exclude: /node_modules/, - loader: 'vue-loader', - options: { - compilerOptions: { - isCustomElement: (tag: string) => webComponents.has(tag), - }, - }, - }, - { - test: /\.js$/i, - exclude: /node_modules/, - use: [ - { - loader: 'esbuild-loader', - options: { - loader: 'js', - target: 'es2020', - }, - }, - ], - }, - { - test: /\.ts$/i, - exclude: /node_modules/, - use: [ - { - loader: 'esbuild-loader', - options: { - loader: 'ts', - target: 'es2020', - }, - }, - ], - }, - { - test: /\.css$/i, - use: [ - { - loader: MiniCssExtractPlugin.loader, - }, - { - loader: 'css-loader', - options: { - sourceMap: sourceMaps === 'true', - url: {filter: filterCssImport}, - import: {filter: filterCssImport}, - importLoaders: 1, - }, - }, - { - loader: 'postcss-loader', - options: { - postcssOptions: { - plugins: [ - tailwindcss(tailwindConfig), - ], - }, - }, - }, - ], - }, - { - test: /\.svg$/i, - include: fileURLToPath(new URL('public/assets/img/svg', import.meta.url)), - type: 'asset/source', - }, - { - test: /\.(ttf|woff2?)$/i, - type: 'asset/resource', - generator: { - filename: 'fonts/[name].[contenthash:8][ext]', - }, - }, - ], - }, - plugins: [ - new DefinePlugin({ - __VUE_OPTIONS_API__: true, // at the moment, many Vue components still use the Vue Options API - __VUE_PROD_DEVTOOLS__: false, // do not enable devtools support in production - __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: false, // https://github.com/vuejs/vue-cli/pull/7443 - }), - // all environment variables used in bundled js via process.env must be declared here - new EnvironmentPlugin({ - TEST: 'false', - }), - new VueLoaderPlugin(), - new MiniCssExtractPlugin({ - filename: 'css/[name].css', - chunkFilename: 'css/[name].[contenthash:8].css', - }), - sourceMaps !== 'false' && new SourceMapDevToolPlugin({ - filename: '[file].[contenthash:8].map', - ...(sourceMaps === 'reduced' && {include: /^js\/index\.js$/}), - }), - new MonacoWebpackPlugin({ - filename: 'js/monaco-[name].[contenthash:8].worker.js', - }), - isProduction ? new LicenseCheckerWebpackPlugin({ - outputFilename: 'licenses.txt', - outputWriter: ({dependencies}: {dependencies: Array>}) => { - const line = '-'.repeat(80); - const goJson = readFileSync('assets/go-licenses.json', 'utf8'); - const goModules = JSON.parse(goJson).map(({name, licenseText}: Record) => { - return {name, body: formatLicenseText(licenseText)}; - }); - const jsModules = dependencies.map(({name, version, licenseName, licenseText}) => { - return {name, version, licenseName, body: formatLicenseText(licenseText)}; - }); - - const modules = [...goModules, ...jsModules].sort((a, b) => a.name.localeCompare(b.name)); - return modules.map(({name, version, licenseName, body}) => { - const title = licenseName ? `${name}@${version} - ${licenseName}` : name; - return `${line}\n${title}\n${line}\n${body}`; - }).join('\n'); - }, - override: { - 'khroma@*': {licenseName: 'MIT'}, // https://github.com/fabiospampinato/khroma/pull/33 - }, - emitError: true, - allow: '(Apache-2.0 OR 0BSD OR BSD-2-Clause OR BSD-3-Clause OR MIT OR ISC OR CPAL-1.0 OR Unlicense OR EPL-1.0 OR EPL-2.0)', - }) : new AddAssetPlugin('licenses.txt', `Licenses are disabled during development`), - ], - performance: { - hints: false, - maxEntrypointSize: Infinity, - maxAssetSize: Infinity, - }, - resolve: { - symlinks: true, - modules: ['node_modules'], - }, - watchOptions: { - ignored: [ - 'node_modules/**', - ], - }, - stats: { - assetsSort: 'name', - assetsSpace: Infinity, - cached: false, - cachedModules: false, - children: false, - chunkModules: false, - chunkOrigins: false, - chunksSort: 'name', - colors: true, - entrypoints: false, - groupAssetsByChunk: false, - groupAssetsByEmitStatus: false, - groupAssetsByInfo: false, - groupModulesByAttributes: false, - modules: false, - reasons: false, - runtimeModules: false, - }, -} satisfies Configuration; From 89b16a150d45c5cb96e4fa28c160205606c4737c Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 13 Mar 2026 17:43:55 +0100 Subject: [PATCH 002/102] Use content-hashed filenames with Vite manifest for cache busting Replace query-string cache busting (?v=) with content-hashed entry filenames for JS and CSS assets. Vite now generates a manifest (.vite/manifest.json) mapping unhashed to hashed paths. A new Go-side AssetPath function reads the manifest and resolves paths in templates. This eliminates the need for the importmap workaround that was required because chunks imported the entry module without the ?v= query parameter. - Add modules/public/manifest.go with mtime-based cache invalidation so dev mode auto-detects frontend rebuilds - Add AssetPath template function for hashed path resolution - Update all templates to use AssetPath instead of ?v=AssetVersion - Pass sharedWorkerPath via window.config for the SharedWorker URL - Rename eventsource.sharedworker to sharedworker - Fix markup_external_test.go for type="module" and hashed paths Co-Authored-By: Claude (Opus 4.6) --- .gitignore | 1 + Makefile | 4 +- eslint.config.ts | 2 +- modules/markup/render.go | 5 +- modules/public/manifest.go | 119 ++++++++++++++++++ modules/public/manifest_test.go | 78 ++++++++++++ modules/templates/helper.go | 2 + templates/base/head_script.tmpl | 6 +- templates/base/head_style.tmpl | 4 +- templates/devtest/devtest-footer.tmpl | 2 +- templates/devtest/devtest-header.tmpl | 2 +- templates/swagger/ui.tmpl | 4 +- tests/integration/markup_external_test.go | 5 +- types.d.ts | 5 - vite.config.ts | 9 +- web_src/js/features/notification.ts | 4 +- ...source.sharedworker.ts => sharedworker.ts} | 0 web_src/js/features/stopwatch.ts | 4 +- web_src/js/globals.d.ts | 2 +- web_src/js/vitest.setup.ts | 2 +- 20 files changed, 226 insertions(+), 34 deletions(-) create mode 100644 modules/public/manifest.go create mode 100644 modules/public/manifest_test.go rename web_src/js/features/{eventsource.sharedworker.ts => sharedworker.ts} (100%) diff --git a/.gitignore b/.gitignore index 45e8e9295fd31..e1f68f887e640 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/Makefile b/Makefile index 29ee94cc5cf20..b8464c60a55a5 100644 --- a/Makefile +++ b/Makefile @@ -122,8 +122,8 @@ MIGRATE_TEST_PACKAGES ?= $(shell $(GO) list code.gitea.io/gitea/models/migration FRONTEND_SOURCES := $(shell find web_src/js web_src/css -type f) FRONTEND_CONFIGS := vite.config.ts tailwind.config.ts -FRONTEND_DEST := public/assets/js/index.js public/assets/js/webcomponents.js public/assets/css/index.css -FRONTEND_DEST_ENTRIES := public/assets/js public/assets/css public/assets/fonts +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.* diff --git a/eslint.config.ts b/eslint.config.ts index a917bb82126b7..1e516ee6a1588 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -372,7 +372,7 @@ export default defineConfig([ 'import-x/no-unresolved': [2, {commonjs: true, ignore: ['\\?.+$']}], 'import-x/no-unused-modules': [0], // incompatible with eslint 9 'import-x/no-useless-path-segments': [2, {commonjs: true}], - 'import-x/no-webpack-loader-syntax': [0], + 'import-x/no-webpack-loader-syntax': [2], 'import-x/order': [0], 'import-x/prefer-default-export': [0], 'import-x/unambiguous': [0], diff --git a/modules/markup/render.go b/modules/markup/render.go index fb7b4cf37cc37..6bfad28090668 100644 --- a/modules/markup/render.go +++ b/modules/markup/render.go @@ -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" @@ -237,8 +238,8 @@ 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.AssetPath("css/external-render-iframe.css") + extraScriptSrc := setting.AppSubURL + "/assets/" + public.AssetPath("js/external-render-iframe.js") // "`, extraScriptSrc, extraStyleHref) } diff --git a/modules/public/manifest.go b/modules/public/manifest.go new file mode 100644 index 0000000000000..b5a0046c8544f --- /dev/null +++ b/modules/public/manifest.go @@ -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 +} + +// AssetPath resolves an unhashed asset path to its content-hashed path from the Vite manifest. +// Example: AssetPath("js/index.js") returns "js/index.C6Z2MRVQ.js" +// Falls back to returning the input path unchanged if the manifest is unavailable. +func AssetPath(name string) string { + paths := getManifestPaths() + if p, ok := paths[name]; ok { + return p + } + return name +} diff --git a/modules/public/manifest_test.go b/modules/public/manifest_test.go new file mode 100644 index 0000000000000..3e3f4e8fc4048 --- /dev/null +++ b/modules/public/manifest_test.go @@ -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 TestAssetPathFallback(t *testing.T) { + // When manifest is not loaded, AssetPath 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", AssetPath("js/index.js")) + assert.Equal(t, "css/theme-gitea-dark.css", AssetPath("css/theme-gitea-dark.css")) +} diff --git a/modules/templates/helper.go b/modules/templates/helper.go index d2d4d364df0df..e103ca72ad298 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -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" @@ -95,6 +96,7 @@ func NewFuncMap() template.FuncMap { "AssetVersion": func() string { return setting.AssetVersion }, + "AssetPath": public.AssetPath, "ShowFooterTemplateLoadTime": func() bool { return setting.Other.ShowFooterTemplateLoadTime }, diff --git a/templates/base/head_script.tmpl b/templates/base/head_script.tmpl index da6b82857b347..580059b9660ba 100644 --- a/templates/base/head_script.tmpl +++ b/templates/base/head_script.tmpl @@ -9,7 +9,6 @@ If you introduce mistakes in it, Gitea JavaScript code wouldn't run correctly. window.config = { appUrl: '{{AppUrl}}', appSubUrl: '{{AppSubUrl}}', - assetVersionEncoded: encodeURIComponent('{{AssetVersion}}'), // will be used in URL construction directly assetUrlPrefix: '{{AssetUrlPrefix}}', runModeIsProd: {{.RunModeIsProd}}, customEmojis: {{CustomEmojis}}, @@ -17,6 +16,7 @@ If you introduce mistakes in it, Gitea JavaScript code wouldn't run correctly. notificationSettings: {{NotificationSettings}}, {{/*a map provided by NewFuncMap in helper.go*/}} enableTimeTracking: {{EnableTimetracking}}, mermaidMaxSourceCharacters: {{MermaidMaxSourceCharacters}}, + sharedWorkerPath: '{{AssetPath "js/sharedworker.js"}}', {{/* this global i18n object should only contain general texts. for specialized texts, it should be provided inside the related modules by: (1) API response (2) HTML data-attribute (3) PageData */}} i18n: { copy_success: {{ctx.Locale.Tr "copy_success"}}, @@ -33,6 +33,4 @@ If you introduce mistakes in it, Gitea JavaScript code wouldn't run correctly. {{/* web components must load as a blocking script to prevent flash of unstyled content */}} -{{/* import map ensures the browser deduplicates the entry module when chunks import it without the ?v= query */}} - - + diff --git a/templates/base/head_style.tmpl b/templates/base/head_style.tmpl index b2fc033558c55..01d7cb18f1df8 100644 --- a/templates/base/head_style.tmpl +++ b/templates/base/head_style.tmpl @@ -1,2 +1,2 @@ - - + + diff --git a/templates/devtest/devtest-footer.tmpl b/templates/devtest/devtest-footer.tmpl index 7a3b421b61181..859f8cb50e477 100644 --- a/templates/devtest/devtest-footer.tmpl +++ b/templates/devtest/devtest-footer.tmpl @@ -1,3 +1,3 @@ {{/* TODO: the devtest.js is isolated from index.js, so no module is shared and many index.js functions do not work in devtest.ts */}} - + {{template "base/footer" ctx.RootData}} diff --git a/templates/devtest/devtest-header.tmpl b/templates/devtest/devtest-header.tmpl index 0775dccc2d78c..7e3baae1c8479 100644 --- a/templates/devtest/devtest-header.tmpl +++ b/templates/devtest/devtest-header.tmpl @@ -1,3 +1,3 @@ {{template "base/head" ctx.RootData}} - + {{template "base/alert" .}} diff --git a/templates/swagger/ui.tmpl b/templates/swagger/ui.tmpl index 338605f5ecac4..a16c032f2098b 100644 --- a/templates/swagger/ui.tmpl +++ b/templates/swagger/ui.tmpl @@ -2,13 +2,13 @@ Gitea API - + {{/* TODO: add Help & Glossary to help users understand the API, and explain some concepts like "Owner" */}} {{svg "octicon-reply"}}{{ctx.Locale.Tr "return_to_gitea"}}
- + diff --git a/tests/integration/markup_external_test.go b/tests/integration/markup_external_test.go index b6932258a6608..ae5728291277f 100644 --- a/tests/integration/markup_external_test.go +++ b/tests/integration/markup_external_test.go @@ -15,6 +15,7 @@ import ( "code.gitea.io/gitea/modules/charset" "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/markup/external" + "code.gitea.io/gitea/modules/public" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/tests" @@ -107,7 +108,7 @@ func TestExternalMarkupRenderer(t *testing.T) { // default sandbox in sub page response assert.Equal(t, "frame-src 'self'; sandbox allow-scripts allow-popups", respSub.Header().Get("Content-Security-Policy")) // FIXME: actually here is a bug (legacy design problem), the "PostProcess" will escape "
<script></script>
`, respSub.Body.String()) + assert.Equal(t, `
<script></script>
`, respSub.Body.String()) }) }) @@ -130,7 +131,7 @@ func TestExternalMarkupRenderer(t *testing.T) { t.Run("HTMLContentWithExternalRenderIframeHelper", func(t *testing.T) { req := NewRequest(t, "GET", "/user2/repo1/render/branch/master/html.no-sanitizer") respSub := MakeRequest(t, req, http.StatusOK) - assert.Equal(t, ``, respSub.Body.String()) + assert.Equal(t, ``, respSub.Body.String()) assert.Equal(t, "frame-src 'self'", respSub.Header().Get("Content-Security-Policy")) }) }) diff --git a/types.d.ts b/types.d.ts index 59d6ecf149f3a..234bd267fe2b0 100644 --- a/types.d.ts +++ b/types.d.ts @@ -1,8 +1,3 @@ -declare module '@techknowlogick/license-checker-webpack-plugin' { - const plugin: any; - export = plugin; -} - declare module 'eslint-plugin-no-use-extend-native' { import type {Eslint} from 'eslint'; const plugin: Eslint.Plugin; diff --git a/vite.config.ts b/vite.config.ts index 1c7167f5af222..f95b80a898b58 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -103,6 +103,7 @@ export default defineConfig({ sourcemap: enableSourcemap, target: buildTarget, minify: isProduction, + manifest: true, chunkSizeWarningLimit: Infinity, reportCompressedSize: false, rolldownOptions: { @@ -114,22 +115,18 @@ export default defineConfig({ index: fileURLToPath(new URL('web_src/js/index.ts', import.meta.url)), swagger: fileURLToPath(new URL('web_src/js/standalone/swagger.ts', import.meta.url)), 'external-render-iframe': fileURLToPath(new URL('web_src/js/standalone/external-render-iframe.ts', import.meta.url)), - 'eventsource.sharedworker': fileURLToPath(new URL('web_src/js/features/eventsource.sharedworker.ts', import.meta.url)), + sharedworker: fileURLToPath(new URL('web_src/js/features/sharedworker.ts', import.meta.url)), ...(!isProduction && { devtest: fileURLToPath(new URL('web_src/js/standalone/devtest.ts', import.meta.url)), }), ...themes, }, output: { - entryFileNames: 'js/[name].js', + entryFileNames: 'js/[name].[hash:8].js', chunkFileNames: 'js/[name].[hash:8].js', assetFileNames: (info: {name?: string}) => { const name = (info.name ?? '').split('?')[0]; if (/\.css$/i.test(name)) { - // css entrypoints with no hash, cache-busted via ?v= in templates - if (/^(index|swagger|external-render-iframe|devtest|theme-.*)\.css$/i.test(name)) { - return `css/${name}`; - } return 'css/[name].[hash:8].css'; } if (/\.(ttf|woff2?)$/i.test(name)) return 'fonts/[name].[hash:8].[ext]'; diff --git a/web_src/js/features/notification.ts b/web_src/js/features/notification.ts index e5246d19b14de..83aa59ec961c3 100644 --- a/web_src/js/features/notification.ts +++ b/web_src/js/features/notification.ts @@ -2,7 +2,7 @@ import {GET} from '../modules/fetch.ts'; import {toggleElem, createElementFromHTML} from '../utils/dom.ts'; import {logoutFromWorker} from '../modules/worker.ts'; -const {appSubUrl, notificationSettings, assetVersionEncoded, assetUrlPrefix} = window.config; +const {appSubUrl, notificationSettings, assetUrlPrefix, sharedWorkerPath} = window.config; let notificationSequenceNumber = 0; async function receiveUpdateCount(event: MessageEvent<{type: string, data: string}>) { @@ -33,7 +33,7 @@ export function initNotificationCount() { if (notificationSettings.EventSourceUpdateTime > 0 && window.EventSource && window.SharedWorker) { // Try to connect to the event source via the shared worker first - const worker = new SharedWorker(`${assetUrlPrefix}/js/eventsource.sharedworker.js?v=${assetVersionEncoded}`, 'notification-worker'); + const worker = new SharedWorker(`${assetUrlPrefix}/${sharedWorkerPath}`, 'notification-worker'); worker.addEventListener('error', (event) => { console.error('worker error', event); }); diff --git a/web_src/js/features/eventsource.sharedworker.ts b/web_src/js/features/sharedworker.ts similarity index 100% rename from web_src/js/features/eventsource.sharedworker.ts rename to web_src/js/features/sharedworker.ts diff --git a/web_src/js/features/stopwatch.ts b/web_src/js/features/stopwatch.ts index ee8b49d2e1f3d..306f50a564de7 100644 --- a/web_src/js/features/stopwatch.ts +++ b/web_src/js/features/stopwatch.ts @@ -3,7 +3,7 @@ import {GET} from '../modules/fetch.ts'; import {hideElem, queryElems, showElem} from '../utils/dom.ts'; import {logoutFromWorker} from '../modules/worker.ts'; -const {appSubUrl, notificationSettings, enableTimeTracking, assetVersionEncoded, assetUrlPrefix} = window.config; +const {appSubUrl, notificationSettings, enableTimeTracking, assetUrlPrefix, sharedWorkerPath} = window.config; export function initStopwatch() { if (!enableTimeTracking) { @@ -47,7 +47,7 @@ export function initStopwatch() { // if the browser supports EventSource and SharedWorker, use it instead of the periodic poller if (notificationSettings.EventSourceUpdateTime > 0 && window.EventSource && window.SharedWorker) { // Try to connect to the event source via the shared worker first - const worker = new SharedWorker(`${assetUrlPrefix}/js/eventsource.sharedworker.js?v=${assetVersionEncoded}`, 'notification-worker'); + const worker = new SharedWorker(`${assetUrlPrefix}/${sharedWorkerPath}`, 'notification-worker'); worker.addEventListener('error', (event) => { console.error('worker error', event); }); diff --git a/web_src/js/globals.d.ts b/web_src/js/globals.d.ts index 688d141deab16..42b82e668e6f6 100644 --- a/web_src/js/globals.d.ts +++ b/web_src/js/globals.d.ts @@ -22,8 +22,8 @@ interface Window { config: { appUrl: string, appSubUrl: string, - assetVersionEncoded: string, assetUrlPrefix: string, + sharedWorkerPath: string, runModeIsProd: boolean, customEmojis: Record, pageData: Record, diff --git a/web_src/js/vitest.setup.ts b/web_src/js/vitest.setup.ts index cc363f3490036..acd0d0318c0a1 100644 --- a/web_src/js/vitest.setup.ts +++ b/web_src/js/vitest.setup.ts @@ -1,8 +1,8 @@ window.config = { appUrl: 'http://localhost:3000/', appSubUrl: '', - assetVersionEncoded: '', assetUrlPrefix: '', + sharedWorkerPath: '', runModeIsProd: true, customEmojis: {}, pageData: {}, From 90374c16f1bcf3f5a9b5cd306a3fe195cb0d5f56 Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 13 Mar 2026 18:07:46 +0100 Subject: [PATCH 003/102] Hash all asset filenames and remove AssetVersion - Add content hash to webcomponents.js IIFE build and append its entry to the Vite manifest - Use AssetPath for swagger asset paths in openapi.go renderer - Strip content hash from theme CSS filenames in webtheme.go to correctly extract internal theme names - Remove AssetVersion variable and template function, now fully replaced by content-hashed filenames via manifest - Rename AssetPath to GetAssetPath Co-Authored-By: Claude (Opus 4.6) --- modules/markup/external/openapi.go | 9 +++++---- modules/markup/render.go | 4 ++-- modules/public/manifest.go | 6 +++--- modules/public/manifest_test.go | 8 ++++---- modules/setting/server.go | 5 ----- modules/templates/helper.go | 5 +---- services/webtheme/webtheme.go | 11 ++++++++++- templates/base/head_script.tmpl | 2 +- templates/status/500.tmpl | 2 +- tests/integration/markup_external_test.go | 4 ++-- vite.config.ts | 23 +++++++++++++++++++---- 11 files changed, 48 insertions(+), 31 deletions(-) diff --git a/modules/markup/external/openapi.go b/modules/markup/external/openapi.go index ac5eae53ffe50..e30382704dd4d 100644 --- a/modules/markup/external/openapi.go +++ b/modules/markup/external/openapi.go @@ -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" ) @@ -61,19 +62,19 @@ func (p *openAPIRenderer) Render(ctx *markup.RenderContext, input io.Reader, out - +
- + `, 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 } diff --git a/modules/markup/render.go b/modules/markup/render.go index 6bfad28090668..fb7879bfc9394 100644 --- a/modules/markup/render.go +++ b/modules/markup/render.go @@ -238,8 +238,8 @@ 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/" + public.AssetPath("css/external-render-iframe.css") - extraScriptSrc := setting.AppSubURL + "/assets/" + public.AssetPath("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") // "`, extraScriptSrc, extraStyleHref) } diff --git a/modules/public/manifest.go b/modules/public/manifest.go index b5a0046c8544f..bd623f9ad85c0 100644 --- a/modules/public/manifest.go +++ b/modules/public/manifest.go @@ -107,10 +107,10 @@ func getManifestPaths() map[string]string { return manifestPaths } -// AssetPath resolves an unhashed asset path to its content-hashed path from the Vite manifest. -// Example: AssetPath("js/index.js") returns "js/index.C6Z2MRVQ.js" +// 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 AssetPath(name string) string { +func GetAssetPath(name string) string { paths := getManifestPaths() if p, ok := paths[name]; ok { return p diff --git a/modules/public/manifest_test.go b/modules/public/manifest_test.go index 3e3f4e8fc4048..481b2e3b38e7c 100644 --- a/modules/public/manifest_test.go +++ b/modules/public/manifest_test.go @@ -61,8 +61,8 @@ func TestParseManifest(t *testing.T) { assert.Empty(t, paths["js/chunk.js"]) } -func TestAssetPathFallback(t *testing.T) { - // When manifest is not loaded, AssetPath should return the input as-is +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) @@ -73,6 +73,6 @@ func TestAssetPathFallback(t *testing.T) { manifestMu.Unlock() }() - assert.Equal(t, "js/index.js", AssetPath("js/index.js")) - assert.Equal(t, "css/theme-gitea-dark.css", AssetPath("css/theme-gitea-dark.css")) + assert.Equal(t, "js/index.js", GetAssetPath("js/index.js")) + assert.Equal(t, "css/theme-gitea-dark.css", GetAssetPath("css/theme-gitea-dark.css")) } diff --git a/modules/setting/server.go b/modules/setting/server.go index 7e7611b802d8d..36342dfdbe9e8 100644 --- a/modules/setting/server.go +++ b/modules/setting/server.go @@ -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 @@ -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) diff --git a/modules/templates/helper.go b/modules/templates/helper.go index e103ca72ad298..c75fdcd779026 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -93,10 +93,7 @@ func NewFuncMap() template.FuncMap { "AppDomain": func() string { // documented in mail-templates.md return setting.Domain }, - "AssetVersion": func() string { - return setting.AssetVersion - }, - "AssetPath": public.AssetPath, + "AssetPath": public.GetAssetPath, "ShowFooterTemplateLoadTime": func() bool { return setting.Other.ShowFooterTemplateLoadTime }, diff --git a/services/webtheme/webtheme.go b/services/webtheme/webtheme.go index a0beec2902af0..6f2767da6641d 100644 --- a/services/webtheme/webtheme.go +++ b/services/webtheme/webtheme.go @@ -108,10 +108,19 @@ func parseThemeMetaInfoToMap(cssContent string) map[string]string { return m } +// stripContentHash removes a Vite content hash suffix from a name. +// e.g. "gitea-dark.CyAaQnn5" -> "gitea-dark" +func stripContentHash(name string) string { + if i := strings.LastIndex(name, "."); i > 0 { + return name[:i] + } + return name +} + func defaultThemeMetaInfoByFileName(fileName string) *ThemeMetaInfo { themeInfo := &ThemeMetaInfo{ FileName: fileName, - InternalName: strings.TrimSuffix(strings.TrimPrefix(fileName, fileNamePrefix), fileNameSuffix), + InternalName: stripContentHash(strings.TrimSuffix(strings.TrimPrefix(fileName, fileNamePrefix), fileNameSuffix)), } themeInfo.DisplayName = themeInfo.InternalName return themeInfo diff --git a/templates/base/head_script.tmpl b/templates/base/head_script.tmpl index 580059b9660ba..97133350ef12f 100644 --- a/templates/base/head_script.tmpl +++ b/templates/base/head_script.tmpl @@ -32,5 +32,5 @@ If you introduce mistakes in it, Gitea JavaScript code wouldn't run correctly. window.config.pageData = window.config.pageData || {}; {{/* web components must load as a blocking script to prevent flash of unstyled content */}} - + diff --git a/templates/status/500.tmpl b/templates/status/500.tmpl index 424f590f84e9a..66b3716a4dd6b 100644 --- a/templates/status/500.tmpl +++ b/templates/status/500.tmpl @@ -1,5 +1,5 @@ {{/* This page should only depend the minimal template functions/variables, to avoid triggering new panics. -* base template functions: AppName, AssetUrlPrefix, AssetVersion, AppSubUrl +* base template functions: AppName, AssetUrlPrefix, AssetPath, AppSubUrl * ctx.Locale * .Flash * .ErrorMsg diff --git a/tests/integration/markup_external_test.go b/tests/integration/markup_external_test.go index ae5728291277f..0e9919d679b9c 100644 --- a/tests/integration/markup_external_test.go +++ b/tests/integration/markup_external_test.go @@ -108,7 +108,7 @@ func TestExternalMarkupRenderer(t *testing.T) { // default sandbox in sub page response assert.Equal(t, "frame-src 'self'; sandbox allow-scripts allow-popups", respSub.Header().Get("Content-Security-Policy")) // FIXME: actually here is a bug (legacy design problem), the "PostProcess" will escape "
<script></script>
`, respSub.Body.String()) + assert.Equal(t, `
<script></script>
`, respSub.Body.String()) }) }) @@ -131,7 +131,7 @@ func TestExternalMarkupRenderer(t *testing.T) { t.Run("HTMLContentWithExternalRenderIframeHelper", func(t *testing.T) { req := NewRequest(t, "GET", "/user2/repo1/render/branch/master/html.no-sanitizer") respSub := MakeRequest(t, req, http.StatusOK) - assert.Equal(t, ``, respSub.Body.String()) + assert.Equal(t, ``, respSub.Body.String()) assert.Equal(t, "frame-src 'self'", respSub.Header().Get("Content-Security-Policy")) }) }) diff --git a/vite.config.ts b/vite.config.ts index f95b80a898b58..aa9ca1119516d 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,9 +1,9 @@ import {build, defineConfig, type Plugin} from 'vite'; import vuePlugin from '@vitejs/plugin-vue'; import {stringPlugin} from 'vite-string-plugin'; -import {readFileSync, globSync} from 'node:fs'; +import {readFileSync, writeFileSync, globSync} from 'node:fs'; import {fileURLToPath} from 'node:url'; -import {parse} from 'node:path'; +import {join, parse} from 'node:path'; import {env} from 'node:process'; import tailwindcss from 'tailwindcss'; import tailwindConfig from './tailwind.config.ts'; @@ -45,7 +45,7 @@ function webcomponentsPlugin(): Plugin { return { name: 'webcomponents-iife', async closeBundle() { - await build({ + const result = await build({ configFile: false, root: import.meta.dirname, publicDir: false, @@ -63,7 +63,7 @@ function webcomponentsPlugin(): Plugin { }, rolldownOptions: { output: { - entryFileNames: 'js/webcomponents.js', + entryFileNames: 'js/webcomponents.[hash:8].js', }, }, }, @@ -74,6 +74,21 @@ function webcomponentsPlugin(): Plugin { stringPlugin(), ], }); + + // Append webcomponents entry to the main Vite manifest + if ('output' in result) { + const entry = result.output.find((o: {fileName: string}) => o.fileName.startsWith('js/webcomponents.')); + if (entry) { + const manifestPath = join(outDir, '.vite', 'manifest.json'); + const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')); + manifest['web_src/js/webcomponents/webcomponents-blocking.ts'] = { + file: entry.fileName, + name: 'webcomponents', + isEntry: true, + }; + writeFileSync(manifestPath, JSON.stringify(manifest, null, 2)); + } + } }, }; } From dea0436a18eca6b50d079627e058aa920d9a1b94 Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 13 Mar 2026 18:25:09 +0100 Subject: [PATCH 004/102] Rename template helper `AssetPath` to `GetAssetPath` and clean up - Rename AssetPath to GetAssetPath in template helper and all templates - Clean up stale webcomponents files before IIFE rebuild - Add comment clarifying TypeError catch for navigation-during-import Co-Authored-By: Claude (Opus 4.6) --- modules/templates/helper.go | 2 +- templates/base/head_script.tmpl | 6 +++--- templates/base/head_style.tmpl | 4 ++-- templates/devtest/devtest-footer.tmpl | 2 +- templates/devtest/devtest-header.tmpl | 2 +- templates/status/500.tmpl | 2 +- templates/swagger/ui.tmpl | 4 ++-- vite.config.ts | 7 ++++++- web_src/js/index.ts | 3 ++- 9 files changed, 19 insertions(+), 13 deletions(-) diff --git a/modules/templates/helper.go b/modules/templates/helper.go index c75fdcd779026..c69624075c568 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -93,7 +93,7 @@ func NewFuncMap() template.FuncMap { "AppDomain": func() string { // documented in mail-templates.md return setting.Domain }, - "AssetPath": public.GetAssetPath, + "GetAssetPath": public.GetAssetPath, "ShowFooterTemplateLoadTime": func() bool { return setting.Other.ShowFooterTemplateLoadTime }, diff --git a/templates/base/head_script.tmpl b/templates/base/head_script.tmpl index 97133350ef12f..1c6d7c9164f1f 100644 --- a/templates/base/head_script.tmpl +++ b/templates/base/head_script.tmpl @@ -16,7 +16,7 @@ If you introduce mistakes in it, Gitea JavaScript code wouldn't run correctly. notificationSettings: {{NotificationSettings}}, {{/*a map provided by NewFuncMap in helper.go*/}} enableTimeTracking: {{EnableTimetracking}}, mermaidMaxSourceCharacters: {{MermaidMaxSourceCharacters}}, - sharedWorkerPath: '{{AssetPath "js/sharedworker.js"}}', + sharedWorkerPath: '{{GetAssetPath "js/sharedworker.js"}}', {{/* this global i18n object should only contain general texts. for specialized texts, it should be provided inside the related modules by: (1) API response (2) HTML data-attribute (3) PageData */}} i18n: { copy_success: {{ctx.Locale.Tr "copy_success"}}, @@ -32,5 +32,5 @@ If you introduce mistakes in it, Gitea JavaScript code wouldn't run correctly. window.config.pageData = window.config.pageData || {}; {{/* web components must load as a blocking script to prevent flash of unstyled content */}} - - + + diff --git a/templates/base/head_style.tmpl b/templates/base/head_style.tmpl index 01d7cb18f1df8..75aaf77dda0b7 100644 --- a/templates/base/head_style.tmpl +++ b/templates/base/head_style.tmpl @@ -1,2 +1,2 @@ - - + + diff --git a/templates/devtest/devtest-footer.tmpl b/templates/devtest/devtest-footer.tmpl index 859f8cb50e477..936874ab7aaac 100644 --- a/templates/devtest/devtest-footer.tmpl +++ b/templates/devtest/devtest-footer.tmpl @@ -1,3 +1,3 @@ {{/* TODO: the devtest.js is isolated from index.js, so no module is shared and many index.js functions do not work in devtest.ts */}} - + {{template "base/footer" ctx.RootData}} diff --git a/templates/devtest/devtest-header.tmpl b/templates/devtest/devtest-header.tmpl index 7e3baae1c8479..e55c51076f554 100644 --- a/templates/devtest/devtest-header.tmpl +++ b/templates/devtest/devtest-header.tmpl @@ -1,3 +1,3 @@ {{template "base/head" ctx.RootData}} - + {{template "base/alert" .}} diff --git a/templates/status/500.tmpl b/templates/status/500.tmpl index 66b3716a4dd6b..9a6727837a5fa 100644 --- a/templates/status/500.tmpl +++ b/templates/status/500.tmpl @@ -1,5 +1,5 @@ {{/* This page should only depend the minimal template functions/variables, to avoid triggering new panics. -* base template functions: AppName, AssetUrlPrefix, AssetPath, AppSubUrl +* base template functions: AppName, AssetUrlPrefix, GetAssetPath, AppSubUrl * ctx.Locale * .Flash * .ErrorMsg diff --git a/templates/swagger/ui.tmpl b/templates/swagger/ui.tmpl index a16c032f2098b..24a90617384a4 100644 --- a/templates/swagger/ui.tmpl +++ b/templates/swagger/ui.tmpl @@ -2,13 +2,13 @@ Gitea API - + {{/* TODO: add Help & Glossary to help users understand the API, and explain some concepts like "Owner" */}} {{svg "octicon-reply"}}{{ctx.Locale.Tr "return_to_gitea"}}
- + diff --git a/vite.config.ts b/vite.config.ts index aa9ca1119516d..10ac1363dbd3f 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,7 +1,7 @@ import {build, defineConfig, type Plugin} from 'vite'; import vuePlugin from '@vitejs/plugin-vue'; import {stringPlugin} from 'vite-string-plugin'; -import {readFileSync, writeFileSync, globSync} from 'node:fs'; +import {readFileSync, writeFileSync, unlinkSync, globSync} from 'node:fs'; import {fileURLToPath} from 'node:url'; import {join, parse} from 'node:path'; import {env} from 'node:process'; @@ -45,6 +45,11 @@ function webcomponentsPlugin(): Plugin { return { name: 'webcomponents-iife', async closeBundle() { + // Clean up old hashed webcomponents files before rebuilding + for (const file of globSync('js/webcomponents.*.js*', {cwd: outDir})) { + unlinkSync(join(outDir, file)); + } + const result = await build({ configFile: false, root: import.meta.dirname, diff --git a/web_src/js/index.ts b/web_src/js/index.ts index e224aa7f2f106..1cde252966b04 100644 --- a/web_src/js/index.ts +++ b/web_src/js/index.ts @@ -20,7 +20,8 @@ onDomReady(async () => { try { await import('./index-domready.ts'); } catch (e) { - // When navigating away before the import completes, a TypeError is thrown + // When navigating away before the dynamic import completes, a TypeError is thrown. + // The error message varies across browsers, so we can't check for a specific string. if (e instanceof TypeError) { console.error('Error loading index-domready:', e); } else { From 581bd1b98bd0e0e1259fac89571471c4c1511a72 Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 13 Mar 2026 18:35:09 +0100 Subject: [PATCH 005/102] cleanup --- vite.config.ts | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/vite.config.ts b/vite.config.ts index 10ac1363dbd3f..6bdba95c4e614 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -105,9 +105,7 @@ function filterCssUrlPlugin(): Plugin { enforce: 'pre', transform(code, id) { if (!id.endsWith('.css') || !id.includes('katex')) return null; - code = code.replace(/,\s*url\([^)]*\.woff\)\s*format\("[^"]*"\)/gi, ''); - code = code.replace(/,\s*url\([^)]*\.ttf\)\s*format\("[^"]*"\)/gi, ''); - return code; + return code.replace(/,\s*url\([^)]*\.(?:woff|ttf)\)\s*format\("[^"]*"\)/gi, ''); }, }; } @@ -155,6 +153,13 @@ export default defineConfig({ }, }, }, + worker: { + rolldownOptions: { + output: { + entryFileNames: 'js/[name].[hash:8].js', + }, + }, + }, css: { transformer: 'postcss', postcss: { @@ -176,13 +181,6 @@ export default defineConfig({ __VUE_PROD_DEVTOOLS__: false, __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: false, }, - worker: { - rolldownOptions: { - output: { - entryFileNames: 'js/[name].[hash:8].js', - }, - }, - }, plugins: [ webcomponentsPlugin(), filterCssUrlPlugin(), From a419e10f10a8c210aee4cab356c5941e49bcb36d Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 13 Mar 2026 18:42:10 +0100 Subject: [PATCH 006/102] Fix webcomponents manifest entry not written `build()` returns an array for IIFE lib builds, so the `'output' in result` check silently skipped the manifest append. Handle both array and single-object return types. Co-Authored-By: Claude (Opus 4.6) --- vite.config.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/vite.config.ts b/vite.config.ts index 6bdba95c4e614..0e127aec7e6a7 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -81,8 +81,10 @@ function webcomponentsPlugin(): Plugin { }); // Append webcomponents entry to the main Vite manifest - if ('output' in result) { - const entry = result.output.find((o: {fileName: string}) => o.fileName.startsWith('js/webcomponents.')); + const results = Array.isArray(result) ? result : [result]; + for (const buildOutput of results) { + if (!('output' in buildOutput)) continue; + const entry = buildOutput.output.find((o: {fileName: string}) => o.fileName.startsWith('js/webcomponents.')); if (entry) { const manifestPath = join(outDir, '.vite', 'manifest.json'); const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')); @@ -92,6 +94,7 @@ function webcomponentsPlugin(): Plugin { isEntry: true, }; writeFileSync(manifestPath, JSON.stringify(manifest, null, 2)); + break; } } }, From e701562f7fba01f505a672d97525e330e2b16cc5 Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 13 Mar 2026 18:43:49 +0100 Subject: [PATCH 007/102] inline --- vite.config.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/vite.config.ts b/vite.config.ts index 0e127aec7e6a7..aee3285b95434 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -81,8 +81,7 @@ function webcomponentsPlugin(): Plugin { }); // Append webcomponents entry to the main Vite manifest - const results = Array.isArray(result) ? result : [result]; - for (const buildOutput of results) { + for (const buildOutput of (Array.isArray(result) ? result : [result])) { if (!('output' in buildOutput)) continue; const entry = buildOutput.output.find((o: {fileName: string}) => o.fileName.startsWith('js/webcomponents.')); if (entry) { From 2a198b0b04635a1eb563870febbceedb28ea4b86 Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 13 Mar 2026 19:00:46 +0100 Subject: [PATCH 008/102] Remove duplicate `webcomponents-blocking.ts` The webcomponents directory had both index.ts and webcomponents-blocking.ts with identical content. Remove the duplicate and use index.ts as the entry. Co-Authored-By: Claude (Opus 4.6) --- vite.config.ts | 4 ++-- web_src/js/webcomponents/webcomponents-blocking.ts | 4 ---- 2 files changed, 2 insertions(+), 6 deletions(-) delete mode 100644 web_src/js/webcomponents/webcomponents-blocking.ts diff --git a/vite.config.ts b/vite.config.ts index aee3285b95434..0db8aa8204742 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -62,7 +62,7 @@ function webcomponentsPlugin(): Plugin { minify: isProduction, reportCompressedSize: false, lib: { - entry: fileURLToPath(new URL('web_src/js/webcomponents/webcomponents-blocking.ts', import.meta.url)), + entry: fileURLToPath(new URL('web_src/js/webcomponents/index.ts', import.meta.url)), formats: ['iife'], name: 'webcomponents', }, @@ -87,7 +87,7 @@ function webcomponentsPlugin(): Plugin { if (entry) { const manifestPath = join(outDir, '.vite', 'manifest.json'); const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')); - manifest['web_src/js/webcomponents/webcomponents-blocking.ts'] = { + manifest['web_src/js/webcomponents/index.ts'] = { file: entry.fileName, name: 'webcomponents', isEntry: true, diff --git a/web_src/js/webcomponents/webcomponents-blocking.ts b/web_src/js/webcomponents/webcomponents-blocking.ts deleted file mode 100644 index f2d725814d77b..0000000000000 --- a/web_src/js/webcomponents/webcomponents-blocking.ts +++ /dev/null @@ -1,4 +0,0 @@ -import './polyfills.ts'; -import './relative-time.ts'; -import './origin-url.ts'; -import './overflow-menu.ts'; From 6533de76f0ad65da2ad883a66eb6bfb9bcf3e0b2 Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 13 Mar 2026 19:06:53 +0100 Subject: [PATCH 009/102] Address review comments on `vite.config.ts` - Add comment explaining ENABLE_SOURCEMAP=reduced backward compatibility - Add dev-licenses-stub plugin so licenses.txt exists in dev builds Co-Authored-By: Claude (Opus 4.6) --- vite.config.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/vite.config.ts b/vite.config.ts index 0db8aa8204742..bc2df9b255ec2 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -12,6 +12,8 @@ import licensePlugin from 'rollup-plugin-license'; const isProduction = env.NODE_ENV !== 'development'; +// ENABLE_SOURCEMAP accepts 'true', 'false', or 'reduced'. +// Vite does not support partial sourcemaps, so 'reduced' is treated as 'true'. let enableSourcemap: boolean; if ('ENABLE_SOURCEMAP' in env) { enableSourcemap = env.ENABLE_SOURCEMAP !== 'false'; @@ -194,7 +196,7 @@ export default defineConfig({ }, }, }), - isProduction && licensePlugin({ + isProduction ? licensePlugin({ thirdParty: { output: { file: fileURLToPath(new URL('public/assets/licenses.txt', import.meta.url)), @@ -220,6 +222,11 @@ export default defineConfig({ return /(Apache-2\.0|0BSD|BSD-2-Clause|BSD-3-Clause|MIT|ISC|CPAL-1\.0|Unlicense|EPL-1\.0|EPL-2\.0)/.test(dependency.license ?? ''); }, }, - }), + }) : { + name: 'dev-licenses-stub', + closeBundle() { + writeFileSync(join(outDir, 'licenses.txt'), 'Licenses are disabled during development'); + }, + }, ], }); From e9554c7802ef3b7d0c22c5619c6b22e4fee80702 Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 13 Mar 2026 19:08:15 +0100 Subject: [PATCH 010/102] Add upstream issue link for ENABLE_SOURCEMAP comment Co-Authored-By: Claude (Opus 4.6) --- vite.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vite.config.ts b/vite.config.ts index bc2df9b255ec2..77416feef67a0 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -13,7 +13,7 @@ import licensePlugin from 'rollup-plugin-license'; const isProduction = env.NODE_ENV !== 'development'; // ENABLE_SOURCEMAP accepts 'true', 'false', or 'reduced'. -// Vite does not support partial sourcemaps, so 'reduced' is treated as 'true'. +// Vite does not support partial sourcemaps (https://github.com/vitejs/vite/issues/19365), so 'reduced' is treated as 'true'. let enableSourcemap: boolean; if ('ENABLE_SOURCEMAP' in env) { enableSourcemap = env.ENABLE_SOURCEMAP !== 'false'; From df3c17a0c9e91ec8ee4149b5f3f6b98e8a549425 Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 13 Mar 2026 19:08:33 +0100 Subject: [PATCH 011/102] add ref --- vite.config.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/vite.config.ts b/vite.config.ts index 77416feef67a0..dd916a62cd5f2 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -13,7 +13,8 @@ import licensePlugin from 'rollup-plugin-license'; const isProduction = env.NODE_ENV !== 'development'; // ENABLE_SOURCEMAP accepts 'true', 'false', or 'reduced'. -// Vite does not support partial sourcemaps (https://github.com/vitejs/vite/issues/19365), so 'reduced' is treated as 'true'. +// Vite does not support partial sourcemaps, so 'reduced' is treated as 'true'. +// https://github.com/vitejs/vite/issues/19365 let enableSourcemap: boolean; if ('ENABLE_SOURCEMAP' in env) { enableSourcemap = env.ENABLE_SOURCEMAP !== 'false'; From 6215c5ee6a845e9be2bb201aff966c5085617a79 Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 13 Mar 2026 19:13:28 +0100 Subject: [PATCH 012/102] Simplify ENABLE_SOURCEMAP handling Co-Authored-By: Claude (Opus 4.6) --- vite.config.ts | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/vite.config.ts b/vite.config.ts index dd916a62cd5f2..db3bedadc17eb 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -12,15 +12,9 @@ import licensePlugin from 'rollup-plugin-license'; const isProduction = env.NODE_ENV !== 'development'; -// ENABLE_SOURCEMAP accepts 'true', 'false', or 'reduced'. -// Vite does not support partial sourcemaps, so 'reduced' is treated as 'true'. -// https://github.com/vitejs/vite/issues/19365 -let enableSourcemap: boolean; -if ('ENABLE_SOURCEMAP' in env) { - enableSourcemap = env.ENABLE_SOURCEMAP !== 'false'; -} else { - enableSourcemap = !isProduction; -} +// ENABLE_SOURCEMAP accepts 'true' or 'false', default is 'true' in dev and 'false' in production. +// Vite does not support partial sourcemaps: https://github.com/vitejs/vite/issues/19365 +const enableSourcemap = env.ENABLE_SOURCEMAP ? env.ENABLE_SOURCEMAP !== 'false' : !isProduction; const outDir = fileURLToPath(new URL('public/assets', import.meta.url)); const buildTarget = 'es2020'; From d6b34b30233bd5890cc39aa1dac27a8e0eb62582 Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 13 Mar 2026 19:43:40 +0100 Subject: [PATCH 013/102] Restore ENABLE_SOURCEMAP=reduced mode with Vite plugin In reduced mode (the production default), only sourcemaps for the index chunks are kept. All other map files including worker maps are deleted after the build. Co-Authored-By: Claude (Opus 4.6) --- vite.config.ts | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/vite.config.ts b/vite.config.ts index db3bedadc17eb..1ffedd9dcac6e 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -12,9 +12,18 @@ import licensePlugin from 'rollup-plugin-license'; const isProduction = env.NODE_ENV !== 'development'; -// ENABLE_SOURCEMAP accepts 'true' or 'false', default is 'true' in dev and 'false' in production. -// Vite does not support partial sourcemaps: https://github.com/vitejs/vite/issues/19365 -const enableSourcemap = env.ENABLE_SOURCEMAP ? env.ENABLE_SOURCEMAP !== 'false' : !isProduction; +// ENABLE_SOURCEMAP accepts the following values: +// true - all enabled, the default in development +// reduced - minimal sourcemaps, the default in production +// false - all disabled +type SourcemapMode = 'true' | 'false' | 'reduced'; +let sourcemapMode: SourcemapMode; +if (env.ENABLE_SOURCEMAP === 'true' || env.ENABLE_SOURCEMAP === 'false' || env.ENABLE_SOURCEMAP === 'reduced') { + sourcemapMode = env.ENABLE_SOURCEMAP; +} else { + sourcemapMode = isProduction ? 'reduced' : 'true'; +} +const enableSourcemap = sourcemapMode !== 'false'; const outDir = fileURLToPath(new URL('public/assets', import.meta.url)); const buildTarget = 'es2020'; @@ -97,6 +106,20 @@ function webcomponentsPlugin(): Plugin { }; } +// In 'reduced' mode, only keep sourcemaps for index chunks +function reducedSourcemapPlugin(): Plugin { + return { + name: 'reduced-sourcemap', + closeBundle() { + for (const file of globSync('**/*.map', {cwd: outDir})) { + if (!/[\\/]index(-domready)?\./.test(file)) { + unlinkSync(join(outDir, file)); + } + } + }, + }; +} + // Filter out legacy font formats from CSS, keeping only woff2 function filterCssUrlPlugin(): Plugin { return { @@ -223,5 +246,6 @@ export default defineConfig({ writeFileSync(join(outDir, 'licenses.txt'), 'Licenses are disabled during development'); }, }, + sourcemapMode === 'reduced' && reducedSourcemapPlugin(), ], }); From 17ab78e7fe538cd2c2b2c13273715a94bdcb5f33 Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 13 Mar 2026 19:52:07 +0100 Subject: [PATCH 014/102] Restore original ENABLE_SOURCEMAP variable names from webpack config Co-Authored-By: Claude (Opus 4.6) --- vite.config.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/vite.config.ts b/vite.config.ts index 1ffedd9dcac6e..acd1a0a04fd37 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -16,14 +16,13 @@ const isProduction = env.NODE_ENV !== 'development'; // true - all enabled, the default in development // reduced - minimal sourcemaps, the default in production // false - all disabled -type SourcemapMode = 'true' | 'false' | 'reduced'; -let sourcemapMode: SourcemapMode; -if (env.ENABLE_SOURCEMAP === 'true' || env.ENABLE_SOURCEMAP === 'false' || env.ENABLE_SOURCEMAP === 'reduced') { - sourcemapMode = env.ENABLE_SOURCEMAP; +let sourceMaps; +if ('ENABLE_SOURCEMAP' in env) { + sourceMaps = ['true', 'false'].includes(env.ENABLE_SOURCEMAP || '') ? env.ENABLE_SOURCEMAP : 'reduced'; } else { - sourcemapMode = isProduction ? 'reduced' : 'true'; + sourceMaps = isProduction ? 'reduced' : 'true'; } -const enableSourcemap = sourcemapMode !== 'false'; +const enableSourcemap = sourceMaps !== 'false'; const outDir = fileURLToPath(new URL('public/assets', import.meta.url)); const buildTarget = 'es2020'; @@ -246,6 +245,6 @@ export default defineConfig({ writeFileSync(join(outDir, 'licenses.txt'), 'Licenses are disabled during development'); }, }, - sourcemapMode === 'reduced' && reducedSourcemapPlugin(), + sourceMaps === 'reduced' && reducedSourcemapPlugin(), ], }); From 5fb3d1584c175ec97b8ff3ad0ea32c2e7323551b Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 13 Mar 2026 20:15:51 +0100 Subject: [PATCH 015/102] Update modules/public/manifest.go Co-authored-by: Lunny Xiao Signed-off-by: silverwind --- modules/public/manifest.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/public/manifest.go b/modules/public/manifest.go index bd623f9ad85c0..bed6e249f9712 100644 --- a/modules/public/manifest.go +++ b/modules/public/manifest.go @@ -1,4 +1,4 @@ -// Copyright 2025 The Gitea Authors. All rights reserved. +// Copyright 2026 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT package public From c52a9da9978bee304fc58753df2d743a06b22b46 Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 13 Mar 2026 20:29:43 +0100 Subject: [PATCH 016/102] Restore ENABLE_SOURCEMAP=reduced mode with Vite plugin In reduced mode (the production default), node_modules are excluded from sourcemaps via a transform hook that empties their mappings. A closeBundle hook then deletes map files with no own code and strips node_modules sourcesContent from mixed maps, reducing total map size from 68 MB to 1.5 MB. Co-Authored-By: Claude (Opus 4.6) --- vite.config.ts | 35 ++++++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/vite.config.ts b/vite.config.ts index acd1a0a04fd37..a319f3dcce3f8 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -105,17 +105,35 @@ function webcomponentsPlugin(): Plugin { }; } -// In 'reduced' mode, only keep sourcemaps for index chunks -function reducedSourcemapPlugin(): Plugin { +// In 'reduced' mode, exclude node_modules from sourcemaps +function reducedSourcemapPlugin(runCloseBundle = false): Plugin { return { name: 'reduced-sourcemap', - closeBundle() { + enforce: 'post', + transform(code, id) { + if (id.includes('node_modules')) { + return {code, map: {mappings: ''}}; + } + return null; + }, + // Delete map files with no own code, strip node_modules sourcesContent from the rest. + // Rolldown ignores generateBundle mutations, so we must rewrite files in closeBundle. + ...(runCloseBundle && {closeBundle() { for (const file of globSync('**/*.map', {cwd: outDir})) { - if (!/[\\/]index(-domready)?\./.test(file)) { - unlinkSync(join(outDir, file)); + const mapPath = join(outDir, file); + const map = JSON.parse(readFileSync(mapPath, 'utf8')); + const hasOwnCode = map.sources?.some((s: string) => !s.includes('node_modules')); + if (!hasOwnCode) { + unlinkSync(mapPath); + continue; } + if (!map.sourcesContent?.length) continue; + map.sourcesContent = map.sourcesContent.map((content: string, i: number) => + map.sources[i]?.includes('node_modules') ? '' : content, + ); + writeFileSync(mapPath, JSON.stringify(map)); } - }, + }}), }; } @@ -175,6 +193,9 @@ export default defineConfig({ }, }, worker: { + plugins: () => [ + sourceMaps === 'reduced' && reducedSourcemapPlugin(), + ], rolldownOptions: { output: { entryFileNames: 'js/[name].[hash:8].js', @@ -206,6 +227,7 @@ export default defineConfig({ webcomponentsPlugin(), filterCssUrlPlugin(), stringPlugin(), + sourceMaps === 'reduced' && reducedSourcemapPlugin(true), vuePlugin({ template: { compilerOptions: { @@ -245,6 +267,5 @@ export default defineConfig({ writeFileSync(join(outDir, 'licenses.txt'), 'Licenses are disabled during development'); }, }, - sourceMaps === 'reduced' && reducedSourcemapPlugin(), ], }); From 3fcea7d1c9a028b3f191dc315f9be0b2c4e6ecce Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 13 Mar 2026 22:02:38 +0100 Subject: [PATCH 017/102] Apply suggestion from @silverwind Signed-off-by: silverwind --- modules/public/manifest_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/public/manifest_test.go b/modules/public/manifest_test.go index 481b2e3b38e7c..672dddfa495c0 100644 --- a/modules/public/manifest_test.go +++ b/modules/public/manifest_test.go @@ -1,4 +1,4 @@ -// Copyright 2025 The Gitea Authors. All rights reserved. +// Copyright 2026 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT package public From 4a83e14d9c74ee79923283cfff5223c8c2fe4b5e Mon Sep 17 00:00:00 2001 From: silverwind Date: Sat, 14 Mar 2026 00:24:59 +0100 Subject: [PATCH 018/102] Build index.js as blocking IIFE, load index-domready as deferred module Replace the separate webcomponents IIFE + deferred module index.js with a single blocking IIFE index.js (containing bootstrap, jQuery, webcomponents, user-settings, htmx) and a deferred module index-domready.js loaded in the footer. This matches webpack's behavior: index.js is a blocking script that sets up globals before body parsing, while heavy features load asynchronously via index-domready. - Build index.ts as IIFE via lib mode in closeBundle, CSS extracted by main build - Add index-domready as separate entry in main build - Redirect jquery imports in main build to window.jQuery (single instance) - Export initGlobalErrorHandler to prevent double-init from shared chunks - Revert inline script type=module changes (blocking index.js makes them unnecessary) - Add index-domready.css to head_style.tmpl for Vue component styles Co-Authored-By: Claude (Opus 4.6) --- package.json | 1 + pnpm-lock.yaml | 306 ++++++++++++++++++++-- templates/base/footer.tmpl | 1 + templates/base/head_script.tmpl | 4 +- templates/base/head_style.tmpl | 1 + templates/repo/diff/box.tmpl | 6 +- templates/shared/combomarkdowneditor.tmpl | 2 +- vite.config.ts | 131 +++++---- web_src/js/bootstrap.ts | 4 +- web_src/js/index.ts | 19 +- 10 files changed, 385 insertions(+), 90 deletions(-) diff --git a/package.json b/package.json index c5142d2d93d3a..e6d32e785b3f8 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "dayjs": "1.11.19", "dropzone": "6.0.0-beta.2", "easymde": "2.20.0", + "esbuild": "0.27.4", "htmx.org": "2.0.8", "idiomorph": "0.7.4", "jquery": "4.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 69e8ff8d324d2..224ea7eee7bdb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -64,7 +64,7 @@ importers: version: 2.1.1(tippy.js@6.3.7)(vue@3.5.29(typescript@5.9.3)) '@vitejs/plugin-vue': specifier: 6.0.5 - version: 6.0.5(vite@8.0.0(@types/node@25.3.5)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2))(vue@3.5.29(typescript@5.9.3)) + version: 6.0.5(vite@8.0.0(@types/node@25.3.5)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2))(vue@3.5.29(typescript@5.9.3)) ansi_up: specifier: 6.0.6 version: 6.0.6 @@ -101,6 +101,9 @@ importers: easymde: specifier: 2.20.0 version: 2.20.0 + esbuild: + specifier: 0.27.4 + version: 0.27.4 htmx.org: specifier: 2.0.8 version: 2.0.8 @@ -166,10 +169,10 @@ importers: version: 0.7.2 vite: specifier: 8.0.0 - version: 8.0.0(@types/node@25.3.5)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2) + version: 8.0.0(@types/node@25.3.5)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2) vite-string-plugin: specifier: 2.0.2 - version: 2.0.2(vite@8.0.0(@types/node@25.3.5)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2)) + version: 2.0.2(vite@8.0.0(@types/node@25.3.5)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2)) vue: specifier: 3.5.29 version: 3.5.29(typescript@5.9.3) @@ -236,7 +239,7 @@ importers: version: 8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) '@vitest/eslint-plugin': specifier: 1.6.9 - version: 1.6.9(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)(vitest@4.1.0(@types/node@25.3.5)(happy-dom@20.8.3)(vite@8.0.0(@types/node@25.3.5)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2))) + version: 1.6.9(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)(vitest@4.1.0(@types/node@25.3.5)(happy-dom@20.8.3)(vite@8.0.0(@types/node@25.3.5)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2))) eslint: specifier: 9.39.2 version: 9.39.2(jiti@2.6.1) @@ -326,7 +329,7 @@ importers: version: 17.8.3 vitest: specifier: 4.1.0 - version: 4.1.0(@types/node@25.3.5)(happy-dom@20.8.3)(vite@8.0.0(@types/node@25.3.5)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2)) + version: 4.1.0(@types/node@25.3.5)(happy-dom@20.8.3)(vite@8.0.0(@types/node@25.3.5)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2)) vue-tsc: specifier: 3.2.5 version: 3.2.5(typescript@5.9.3) @@ -485,6 +488,162 @@ packages: '@emnapi/wasi-threads@1.1.0': resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} + '@esbuild/aix-ppc64@0.27.4': + resolution: {integrity: sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.4': + resolution: {integrity: sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.4': + resolution: {integrity: sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.4': + resolution: {integrity: sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.4': + resolution: {integrity: sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.4': + resolution: {integrity: sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.4': + resolution: {integrity: sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.4': + resolution: {integrity: sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.4': + resolution: {integrity: sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.4': + resolution: {integrity: sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.4': + resolution: {integrity: sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.4': + resolution: {integrity: sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.4': + resolution: {integrity: sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.4': + resolution: {integrity: sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.4': + resolution: {integrity: sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.4': + resolution: {integrity: sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.4': + resolution: {integrity: sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.4': + resolution: {integrity: sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.4': + resolution: {integrity: sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.4': + resolution: {integrity: sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.4': + resolution: {integrity: sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.4': + resolution: {integrity: sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.4': + resolution: {integrity: sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.4': + resolution: {integrity: sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.4': + resolution: {integrity: sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.4': + resolution: {integrity: sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@eslint-community/eslint-plugin-eslint-comments@4.7.1': resolution: {integrity: sha512-Ql2nJFwA8wUGpILYGOQaT1glPsmvEwE0d+a+l7AALLzQvInqdbXJdx7aSu0DpUX9dB1wMVBMhm99/++S3MdEtQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -2065,6 +2224,11 @@ packages: es-module-lexer@2.0.0: resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==} + esbuild@0.27.4: + resolution: {integrity: sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -4057,6 +4221,84 @@ snapshots: tslib: 2.8.1 optional: true + '@esbuild/aix-ppc64@0.27.4': + optional: true + + '@esbuild/android-arm64@0.27.4': + optional: true + + '@esbuild/android-arm@0.27.4': + optional: true + + '@esbuild/android-x64@0.27.4': + optional: true + + '@esbuild/darwin-arm64@0.27.4': + optional: true + + '@esbuild/darwin-x64@0.27.4': + optional: true + + '@esbuild/freebsd-arm64@0.27.4': + optional: true + + '@esbuild/freebsd-x64@0.27.4': + optional: true + + '@esbuild/linux-arm64@0.27.4': + optional: true + + '@esbuild/linux-arm@0.27.4': + optional: true + + '@esbuild/linux-ia32@0.27.4': + optional: true + + '@esbuild/linux-loong64@0.27.4': + optional: true + + '@esbuild/linux-mips64el@0.27.4': + optional: true + + '@esbuild/linux-ppc64@0.27.4': + optional: true + + '@esbuild/linux-riscv64@0.27.4': + optional: true + + '@esbuild/linux-s390x@0.27.4': + optional: true + + '@esbuild/linux-x64@0.27.4': + optional: true + + '@esbuild/netbsd-arm64@0.27.4': + optional: true + + '@esbuild/netbsd-x64@0.27.4': + optional: true + + '@esbuild/openbsd-arm64@0.27.4': + optional: true + + '@esbuild/openbsd-x64@0.27.4': + optional: true + + '@esbuild/openharmony-arm64@0.27.4': + optional: true + + '@esbuild/sunos-x64@0.27.4': + optional: true + + '@esbuild/win32-arm64@0.27.4': + optional: true + + '@esbuild/win32-ia32@0.27.4': + optional: true + + '@esbuild/win32-x64@0.27.4': + optional: true + '@eslint-community/eslint-plugin-eslint-comments@4.7.1(eslint@9.39.2(jiti@2.6.1))': dependencies: escape-string-regexp: 4.0.0 @@ -4826,20 +5068,20 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.11.1': optional: true - '@vitejs/plugin-vue@6.0.5(vite@8.0.0(@types/node@25.3.5)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2))(vue@3.5.29(typescript@5.9.3))': + '@vitejs/plugin-vue@6.0.5(vite@8.0.0(@types/node@25.3.5)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2))(vue@3.5.29(typescript@5.9.3))': dependencies: '@rolldown/pluginutils': 1.0.0-rc.2 - vite: 8.0.0(@types/node@25.3.5)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2) + vite: 8.0.0(@types/node@25.3.5)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2) vue: 3.5.29(typescript@5.9.3) - '@vitest/eslint-plugin@1.6.9(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)(vitest@4.1.0(@types/node@25.3.5)(happy-dom@20.8.3)(vite@8.0.0(@types/node@25.3.5)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2)))': + '@vitest/eslint-plugin@1.6.9(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)(vitest@4.1.0(@types/node@25.3.5)(happy-dom@20.8.3)(vite@8.0.0(@types/node@25.3.5)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2)))': dependencies: '@typescript-eslint/scope-manager': 8.56.1 '@typescript-eslint/utils': 8.56.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.2(jiti@2.6.1) optionalDependencies: typescript: 5.9.3 - vitest: 4.1.0(@types/node@25.3.5)(happy-dom@20.8.3)(vite@8.0.0(@types/node@25.3.5)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2)) + vitest: 4.1.0(@types/node@25.3.5)(happy-dom@20.8.3)(vite@8.0.0(@types/node@25.3.5)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2)) transitivePeerDependencies: - supports-color @@ -4852,13 +5094,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.0.3 - '@vitest/mocker@4.1.0(vite@8.0.0(@types/node@25.3.5)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2))': + '@vitest/mocker@4.1.0(vite@8.0.0(@types/node@25.3.5)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2))': dependencies: '@vitest/spy': 4.1.0 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.0.0(@types/node@25.3.5)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2) + vite: 8.0.0(@types/node@25.3.5)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2) '@vitest/pretty-format@4.1.0': dependencies: @@ -5544,6 +5786,35 @@ snapshots: es-module-lexer@2.0.0: {} + esbuild@0.27.4: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.4 + '@esbuild/android-arm': 0.27.4 + '@esbuild/android-arm64': 0.27.4 + '@esbuild/android-x64': 0.27.4 + '@esbuild/darwin-arm64': 0.27.4 + '@esbuild/darwin-x64': 0.27.4 + '@esbuild/freebsd-arm64': 0.27.4 + '@esbuild/freebsd-x64': 0.27.4 + '@esbuild/linux-arm': 0.27.4 + '@esbuild/linux-arm64': 0.27.4 + '@esbuild/linux-ia32': 0.27.4 + '@esbuild/linux-loong64': 0.27.4 + '@esbuild/linux-mips64el': 0.27.4 + '@esbuild/linux-ppc64': 0.27.4 + '@esbuild/linux-riscv64': 0.27.4 + '@esbuild/linux-s390x': 0.27.4 + '@esbuild/linux-x64': 0.27.4 + '@esbuild/netbsd-arm64': 0.27.4 + '@esbuild/netbsd-x64': 0.27.4 + '@esbuild/openbsd-arm64': 0.27.4 + '@esbuild/openbsd-x64': 0.27.4 + '@esbuild/openharmony-arm64': 0.27.4 + '@esbuild/sunos-x64': 0.27.4 + '@esbuild/win32-arm64': 0.27.4 + '@esbuild/win32-ia32': 0.27.4 + '@esbuild/win32-x64': 0.27.4 + escalade@3.2.0: {} escape-string-regexp@1.0.5: {} @@ -7372,11 +7643,11 @@ snapshots: vanilla-colorful@0.7.2: {} - vite-string-plugin@2.0.2(vite@8.0.0(@types/node@25.3.5)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2)): + vite-string-plugin@2.0.2(vite@8.0.0(@types/node@25.3.5)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2)): dependencies: - vite: 8.0.0(@types/node@25.3.5)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2) + vite: 8.0.0(@types/node@25.3.5)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2) - vite@8.0.0(@types/node@25.3.5)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2): + vite@8.0.0(@types/node@25.3.5)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2): dependencies: '@oxc-project/runtime': 0.115.0 lightningcss: 1.32.0 @@ -7386,15 +7657,16 @@ snapshots: tinyglobby: 0.2.15 optionalDependencies: '@types/node': 25.3.5 + esbuild: 0.27.4 fsevents: 2.3.3 jiti: 2.6.1 terser: 5.46.0 yaml: 2.8.2 - vitest@4.1.0(@types/node@25.3.5)(happy-dom@20.8.3)(vite@8.0.0(@types/node@25.3.5)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2)): + vitest@4.1.0(@types/node@25.3.5)(happy-dom@20.8.3)(vite@8.0.0(@types/node@25.3.5)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2)): dependencies: '@vitest/expect': 4.1.0 - '@vitest/mocker': 4.1.0(vite@8.0.0(@types/node@25.3.5)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2)) + '@vitest/mocker': 4.1.0(vite@8.0.0(@types/node@25.3.5)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2)) '@vitest/pretty-format': 4.1.0 '@vitest/runner': 4.1.0 '@vitest/snapshot': 4.1.0 @@ -7411,7 +7683,7 @@ snapshots: tinyexec: 1.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 - vite: 8.0.0(@types/node@25.3.5)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2) + vite: 8.0.0(@types/node@25.3.5)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 25.3.5 diff --git a/templates/base/footer.tmpl b/templates/base/footer.tmpl index 3af66e736990b..b312d243a15a0 100644 --- a/templates/base/footer.tmpl +++ b/templates/base/footer.tmpl @@ -9,6 +9,7 @@ {{template "custom/body_outer_post" .}} {{template "base/footer_content" .}} + {{template "custom/footer" .}} diff --git a/templates/base/head_script.tmpl b/templates/base/head_script.tmpl index 1c6d7c9164f1f..ae677e3f90b4b 100644 --- a/templates/base/head_script.tmpl +++ b/templates/base/head_script.tmpl @@ -31,6 +31,4 @@ If you introduce mistakes in it, Gitea JavaScript code wouldn't run correctly. {{/* in case some pages don't render the pageData, we make sure it is an object to prevent null access */}} window.config.pageData = window.config.pageData || {}; -{{/* web components must load as a blocking script to prevent flash of unstyled content */}} - - + diff --git a/templates/base/head_style.tmpl b/templates/base/head_style.tmpl index 75aaf77dda0b7..556f656e4a374 100644 --- a/templates/base/head_style.tmpl +++ b/templates/base/head_style.tmpl @@ -1,2 +1,3 @@ + diff --git a/templates/repo/diff/box.tmpl b/templates/repo/diff/box.tmpl index 690e11ff48d04..390e41ec340cb 100644 --- a/templates/repo/diff/box.tmpl +++ b/templates/repo/diff/box.tmpl @@ -8,14 +8,13 @@ {{svg "octicon-sidebar-collapse" 20 "icon tw-hidden"}} {{svg "octicon-sidebar-expand" 20 "icon tw-hidden"}} - {{end}} {{if not .DiffNotAvailable}} @@ -63,6 +62,9 @@ {{if $showFileTree}} {{$.FileIconPoolHTML}}
+ {{end}} {{if .DiffNotAvailable}}

{{ctx.Locale.Tr "repo.diff.data_not_available"}}

diff --git a/templates/shared/combomarkdowneditor.tmpl b/templates/shared/combomarkdowneditor.tmpl index 2c394a6dc11f4..55da6db9b48ac 100644 --- a/templates/shared/combomarkdowneditor.tmpl +++ b/templates/shared/combomarkdowneditor.tmpl @@ -75,7 +75,7 @@ {{if .DisableAutosize}}data-disable-autosize="{{.DisableAutosize}}"{{end}} >{{.TextareaContent}} - - + diff --git a/templates/base/head_style.tmpl b/templates/base/head_style.tmpl index 556f656e4a374..7446d4f639e1f 100644 --- a/templates/base/head_style.tmpl +++ b/templates/base/head_style.tmpl @@ -1,3 +1,2 @@ - diff --git a/vite.config.ts b/vite.config.ts index 55878b83eec98..085b7302a3294 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -71,18 +71,11 @@ function commonViteOpts(opts: T): T { } // Build index.js as a blocking IIFE bundle, matching the pre-Vite webpack behavior. -// CSS is handled by the main build (index.ts remains an entry there for CSS extraction). function iifeIndexPlugin(): Plugin { return { name: 'iife-index', async closeBundle() { - // Save CSS references from the main build's manifest before replacing the entry - const manifestPath = join(outDir, '.vite', 'manifest.json'); - let manifest: Record = {}; - try { manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) } catch {} - const mainIndexCss = manifest['web_src/js/index.ts']?.css || []; - - // Clean up main build's index.js (replaced by IIFE) and old webcomponents files + // Clean up old hashed files before rebuilding for (const file of globSync('js/index.*.js*', {cwd: outDir})) unlinkSync(join(outDir, file)); for (const file of globSync('js/webcomponents.*.js*', {cwd: outDir})) unlinkSync(join(outDir, file)); @@ -92,7 +85,6 @@ function iifeIndexPlugin(): Plugin { entry: fileURLToPath(new URL('web_src/js/index.ts', import.meta.url)), formats: ['iife'], name: 'gitea', - cssFileName: 'index', }, rolldownOptions: { output: { @@ -104,20 +96,15 @@ function iifeIndexPlugin(): Plugin { 'process.env.NODE_ENV': JSON.stringify(isProduction ? 'production' : 'development'), }, plugins: [ - // CSS is extracted by the main build, strip it here to avoid duplication - { - name: 'strip-css', - transform(_code: string, id: string) { - if (id.endsWith('.css')) return {code: '', map: null}; - return null; - }, - }, stringPlugin(), sourceMaps === 'reduced' && reducedSourcemapPlugin(), ], })); - // Update manifest: IIFE index.js + CSS from main build + // Append IIFE index entry to the main Vite manifest + const manifestPath = join(outDir, '.vite', 'manifest.json'); + let manifest: Record = {}; + try { manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) } catch {} for (const buildOutput of (Array.isArray(result) ? result : [result])) { if (!('output' in buildOutput)) continue; const entry = buildOutput.output.find((o: {fileName: string}) => o.fileName.startsWith('js/index.')); @@ -126,7 +113,6 @@ function iifeIndexPlugin(): Plugin { file: entry.fileName, name: 'index', isEntry: true, - ...(mainIndexCss.length && {css: mainIndexCss}), }; delete manifest['web_src/js/webcomponents/index.ts']; writeFileSync(manifestPath, JSON.stringify(manifest, null, 2)); @@ -189,7 +175,6 @@ export default defineConfig(commonViteOpts({ chunkSizeWarningLimit: Infinity, rolldownOptions: { input: { - index: fileURLToPath(new URL('web_src/js/index.ts', import.meta.url)), 'index-domready': fileURLToPath(new URL('web_src/js/index-domready.ts', import.meta.url)), swagger: fileURLToPath(new URL('web_src/js/standalone/swagger.ts', import.meta.url)), 'external-render-iframe': fileURLToPath(new URL('web_src/js/standalone/external-render-iframe.ts', import.meta.url)), @@ -246,20 +231,6 @@ export default defineConfig(commonViteOpts({ }, plugins: [ iifeIndexPlugin(), - // jQuery is bundled in the IIFE index.js and set as window.jQuery. - // Redirect imports in the main build to use that single instance. - { - name: 'jquery-global', - enforce: 'pre' as const, - resolveId(id: string) { - if (id === 'jquery') return '\0jquery-global'; - return null; - }, - load(id: string) { - if (id === '\0jquery-global') return 'export default window.jQuery'; - return null; - }, - }, filterCssUrlPlugin(), stringPlugin(), sourceMaps === 'reduced' && reducedSourcemapPlugin(true), diff --git a/web_src/js/index-domready.ts b/web_src/js/index-domready.ts index fb445b8df42a2..0c8f90a17b039 100644 --- a/web_src/js/index-domready.ts +++ b/web_src/js/index-domready.ts @@ -1,4 +1,6 @@ import '../fomantic/build/fomantic.js'; +import '../fomantic/build/fomantic.css'; +import '../css/index.css'; import {initHtmx} from './htmx.ts'; import {initDashboardRepoList} from './features/dashboard.ts'; diff --git a/web_src/js/index.ts b/web_src/js/index.ts index 7d0a80ab8803a..707a5525e0c65 100644 --- a/web_src/js/index.ts +++ b/web_src/js/index.ts @@ -1,7 +1,5 @@ // bootstrap module must be the first one to be imported, it handles global errors import {initGlobalErrorHandler} from './bootstrap.ts'; -import '../fomantic/build/fomantic.css'; -import '../css/index.css'; // many users expect to use jQuery in their custom scripts (https://docs.gitea.com/administration/customizing-gitea#example-plantuml) // so load globals (including jQuery) as early as possible diff --git a/web_src/js/modules/fomantic.ts b/web_src/js/modules/fomantic.ts index 4b1dbc4f62665..ee45f676ba4d0 100644 --- a/web_src/js/modules/fomantic.ts +++ b/web_src/js/modules/fomantic.ts @@ -1,4 +1,3 @@ -import $ from 'jquery'; import {initAriaCheckboxPatch} from './fomantic/checkbox.ts'; import {initAriaFormFieldPatch} from './fomantic/form.ts'; import {initAriaDropdownPatch} from './fomantic/dropdown.ts'; diff --git a/web_src/js/modules/fomantic/base.ts b/web_src/js/modules/fomantic/base.ts index a227d8123a3a9..f3953e60cdd11 100644 --- a/web_src/js/modules/fomantic/base.ts +++ b/web_src/js/modules/fomantic/base.ts @@ -1,4 +1,3 @@ -import $ from 'jquery'; import {generateElemId} from '../../utils/dom.ts'; export function linkLabelAndInput(label: Element, input: Element) { diff --git a/web_src/js/modules/fomantic/dimmer.ts b/web_src/js/modules/fomantic/dimmer.ts index cbdfac23cba53..6782f0137d930 100644 --- a/web_src/js/modules/fomantic/dimmer.ts +++ b/web_src/js/modules/fomantic/dimmer.ts @@ -1,4 +1,3 @@ -import $ from 'jquery'; import {queryElemChildren} from '../../utils/dom.ts'; export function initFomanticDimmer() { diff --git a/web_src/js/modules/fomantic/dropdown.ts b/web_src/js/modules/fomantic/dropdown.ts index 7f7f3611bebcf..b98a5cf3f4166 100644 --- a/web_src/js/modules/fomantic/dropdown.ts +++ b/web_src/js/modules/fomantic/dropdown.ts @@ -1,4 +1,3 @@ -import $ from 'jquery'; import type {FomanticInitFunction} from '../../types.ts'; import {generateElemId, queryElems} from '../../utils/dom.ts'; diff --git a/web_src/js/modules/fomantic/modal.ts b/web_src/js/modules/fomantic/modal.ts index a96c7785e1ae0..1383692c9858b 100644 --- a/web_src/js/modules/fomantic/modal.ts +++ b/web_src/js/modules/fomantic/modal.ts @@ -1,4 +1,3 @@ -import $ from 'jquery'; import type {FomanticInitFunction} from '../../types.ts'; import {queryElems} from '../../utils/dom.ts'; import {hideToastsFrom} from '../toast.ts'; diff --git a/web_src/js/modules/fomantic/tab.ts b/web_src/js/modules/fomantic/tab.ts index b9578c96375d8..4d1bd7e648d5d 100644 --- a/web_src/js/modules/fomantic/tab.ts +++ b/web_src/js/modules/fomantic/tab.ts @@ -1,4 +1,3 @@ -import $ from 'jquery'; import {queryElemSiblings} from '../../utils/dom.ts'; export function initFomanticTab() { diff --git a/web_src/js/modules/fomantic/transition.ts b/web_src/js/modules/fomantic/transition.ts index 52c407c9c0d85..c4eb1d75e90fb 100644 --- a/web_src/js/modules/fomantic/transition.ts +++ b/web_src/js/modules/fomantic/transition.ts @@ -1,5 +1,3 @@ -import $ from 'jquery'; - export function initFomanticTransition() { const transitionNopBehaviors = new Set([ 'clear queue', 'stop', 'stop all', 'destroy', From e13f106528fda6de7fd5b47c21f27f697073826f Mon Sep 17 00:00:00 2001 From: silverwind Date: Sat, 14 Mar 2026 00:47:13 +0100 Subject: [PATCH 020/102] Use AssetFS() for Vite manifest reading Replace os.Stat/os.ReadFile with AssetFS().Open() which handles both disk files and embedded bindata, respecting StaticRootPath. Co-Authored-By: Claude (Opus 4.6) --- modules/public/manifest.go | 51 +++++++++++++++++++------------------- 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/modules/public/manifest.go b/modules/public/manifest.go index bed6e249f9712..fd15c46578fa8 100644 --- a/modules/public/manifest.go +++ b/modules/public/manifest.go @@ -4,14 +4,12 @@ package public import ( - "os" + "io" "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 { @@ -27,10 +25,6 @@ var ( 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 { @@ -58,15 +52,18 @@ func parseManifest(data []byte) map[string]string { } 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 + f, err := AssetFS().Open("assets/.vite/manifest.json") + if err != nil { manifestMu.RUnlock() - return paths + return manifestPaths + } + fi, err := f.Stat() + f.Close() + if err != nil || fi.ModTime().UnixNano() == manifestModTime { + manifestMu.RUnlock() + return manifestPaths } } manifestMu.RUnlock() @@ -75,24 +72,26 @@ func getManifestPaths() map[string]string { 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 + f, err := AssetFS().Open("assets/.vite/manifest.json") + if err != nil { + log.Error("Failed to open Vite manifest: %v", err) + if manifestPaths == nil { + manifestPaths = make(map[string]string) } + 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") + fi, err := f.Stat() + if err == nil && manifestPaths != nil && fi.ModTime().UnixNano() == manifestModTime { + f.Close() + return manifestPaths } + data, err := io.ReadAll(f) + f.Close() if err != nil { log.Error("Failed to read Vite manifest: %v", err) - manifestPaths = make(map[string]string) + if manifestPaths == nil { + manifestPaths = make(map[string]string) + } return manifestPaths } From 745cc7d7d0e9a29cd7e91a340659f07965e08990 Mon Sep 17 00:00:00 2001 From: silverwind Date: Sat, 14 Mar 2026 01:09:56 +0100 Subject: [PATCH 021/102] Simplify manifest loading, remove htmx from IIFE MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use sync.Once + cached AssetFS for manifest loading. In production the manifest is loaded once with zero per-request overhead. In dev mode, mod time is checked for watch-frontend hot reload. Remove htmx import from index.ts (IIFE) — it was being loaded twice (IIFE + index-domready), causing htmx to process hx-* attributes twice and breaking pages with htmx content. htmx in index-domready (deferred module) executes before DOMContentLoaded, so the init timing is correct. IIFE size: 204KB -> 134KB. Co-Authored-By: Claude (Opus 4.6) --- modules/public/manifest.go | 83 ++++++++++++++++----------------- modules/public/manifest_test.go | 8 +--- web_src/js/index.ts | 5 -- 3 files changed, 41 insertions(+), 55 deletions(-) diff --git a/modules/public/manifest.go b/modules/public/manifest.go index fd15c46578fa8..caa9dcac7ad9c 100644 --- a/modules/public/manifest.go +++ b/modules/public/manifest.go @@ -8,8 +8,10 @@ import ( "path" "sync" + "code.gitea.io/gitea/modules/assetfs" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" ) type viteManifestEntry struct { @@ -20,16 +22,19 @@ type viteManifestEntry struct { } var ( - manifestMu sync.RWMutex + manifestOnce sync.Once + manifestFS *assetfs.LayeredFS manifestPaths map[string]string manifestModTime int64 ) +const manifestPath = "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 + return make(map[string]string) } paths := make(map[string]string) @@ -42,7 +47,7 @@ func parseManifest(data []byte) map[string]string { 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" + // Map associated CSS files, e.g. "css/index-domready.css" -> "css/index-domready.B3zrQPqD.css" for _, css := range entry.CSS { cssKey := path.Dir(css) + "/" + entry.Name + path.Ext(css) paths[cssKey] = css @@ -51,57 +56,49 @@ func parseManifest(data []byte) map[string]string { return paths } -func getManifestPaths() map[string]string { - manifestMu.RLock() - if manifestPaths != nil { - f, err := AssetFS().Open("assets/.vite/manifest.json") - if err != nil { - manifestMu.RUnlock() - return manifestPaths - } - fi, err := f.Stat() - f.Close() - if err != nil || fi.ModTime().UnixNano() == manifestModTime { - manifestMu.RUnlock() - return manifestPaths - } - } - manifestMu.RUnlock() - - manifestMu.Lock() - defer manifestMu.Unlock() +func initManifest() { + manifestFS = AssetFS() + reloadManifest() +} - // Double-check after acquiring write lock - f, err := AssetFS().Open("assets/.vite/manifest.json") +func reloadManifest() { + f, err := manifestFS.Open(manifestPath) if err != nil { log.Error("Failed to open Vite manifest: %v", err) - if manifestPaths == nil { - manifestPaths = make(map[string]string) - } - return manifestPaths + manifestPaths = make(map[string]string) + return } + defer f.Close() + fi, err := f.Stat() - if err == nil && manifestPaths != nil && fi.ModTime().UnixNano() == manifestModTime { - f.Close() - return manifestPaths + if err == nil { + manifestModTime = fi.ModTime().UnixNano() } + data, err := io.ReadAll(f) - f.Close() if err != nil { log.Error("Failed to read Vite manifest: %v", err) - if manifestPaths == nil { - manifestPaths = make(map[string]string) - } - return manifestPaths + manifestPaths = make(map[string]string) + return } - paths := parseManifest(data) - if paths == nil { - paths = make(map[string]string) - } - manifestPaths = paths - if fi != nil { - manifestModTime = fi.ModTime().UnixNano() + manifestPaths = parseManifest(data) +} + +func getManifestPaths() map[string]string { + manifestOnce.Do(initManifest) + + // In production the manifest is immutable (embedded in the binary). + // In dev mode, check if it changed on disk (for watch-frontend). + if !setting.IsProd { + f, err := manifestFS.Open(manifestPath) + if err == nil { + fi, err := f.Stat() + f.Close() + if err == nil && fi.ModTime().UnixNano() != manifestModTime { + reloadManifest() + } + } } return manifestPaths } diff --git a/modules/public/manifest_test.go b/modules/public/manifest_test.go index 672dddfa495c0..0f8af13cbb331 100644 --- a/modules/public/manifest_test.go +++ b/modules/public/manifest_test.go @@ -63,15 +63,9 @@ func TestParseManifest(t *testing.T) { 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() - }() + defer func() { manifestPaths = old }() assert.Equal(t, "js/index.js", GetAssetPath("js/index.js")) assert.Equal(t, "css/theme-gitea-dark.css", GetAssetPath("css/theme-gitea-dark.css")) diff --git a/web_src/js/index.ts b/web_src/js/index.ts index 707a5525e0c65..1c3987ff00622 100644 --- a/web_src/js/index.ts +++ b/web_src/js/index.ts @@ -8,9 +8,4 @@ import './globals.ts'; import './webcomponents/index.ts'; import './modules/user-settings.ts'; // templates also need to use localUserSettings in inline scripts -// TODO: There is a bug in htmx, it incorrectly checks "readyState === 'complete'" when the DOM tree is ready and won't trigger DOMContentLoaded -// Then importing the htmx in our onDomReady will make htmx skip its initialization. -// If the bug would be fixed (https://github.com/bigskysoftware/htmx/pull/3365), then we can only import htmx in "onDomReady" -import 'htmx.org'; - initGlobalErrorHandler(); From 70568ddf68ca0cec5c7f378c7027b43606ac8d02 Mon Sep 17 00:00:00 2001 From: silverwind Date: Sat, 14 Mar 2026 01:15:59 +0100 Subject: [PATCH 022/102] Use Vite's Manifest type for manifest handling Co-Authored-By: Claude (Opus 4.6) --- vite.config.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/vite.config.ts b/vite.config.ts index 085b7302a3294..c7068f61dfe9a 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,4 +1,4 @@ -import {build, defineConfig, type InlineConfig, type Plugin} from 'vite'; +import {build, defineConfig, type InlineConfig, type Manifest, type Plugin} from 'vite'; import vuePlugin from '@vitejs/plugin-vue'; import {stringPlugin} from 'vite-string-plugin'; import {readFileSync, writeFileSync, unlinkSync, globSync} from 'node:fs'; @@ -42,8 +42,7 @@ const webComponents = new Set([ const formatLicenseText = (licenseText: string) => wrapAnsi(licenseText || '', 80).trim(); -function commonViteOpts(opts: T): T { - const {build, ...rest} = opts; +function commonViteOpts({build, ...rest}: T): T { const {rolldownOptions, ...buildRest} = build || {}; return { configFile: false, @@ -103,11 +102,11 @@ function iifeIndexPlugin(): Plugin { // Append IIFE index entry to the main Vite manifest const manifestPath = join(outDir, '.vite', 'manifest.json'); - let manifest: Record = {}; + let manifest: Manifest = {}; try { manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) } catch {} for (const buildOutput of (Array.isArray(result) ? result : [result])) { if (!('output' in buildOutput)) continue; - const entry = buildOutput.output.find((o: {fileName: string}) => o.fileName.startsWith('js/index.')); + const entry = buildOutput.output.find((o) => o.fileName.startsWith('js/index.')); if (entry) { manifest['web_src/js/index.ts'] = { file: entry.fileName, From e0b1ad884c7e3c421f85375ec3c3326ba6a5180d Mon Sep 17 00:00:00 2001 From: silverwind Date: Sat, 14 Mar 2026 01:21:55 +0100 Subject: [PATCH 023/102] Add jQuery to vitest setup for global $ in tests Co-Authored-By: Claude (Opus 4.6) --- web_src/js/vitest.setup.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/web_src/js/vitest.setup.ts b/web_src/js/vitest.setup.ts index acd0d0318c0a1..099a90158bfda 100644 --- a/web_src/js/vitest.setup.ts +++ b/web_src/js/vitest.setup.ts @@ -1,3 +1,5 @@ +import './globals.ts'; + window.config = { appUrl: 'http://localhost:3000/', appSubUrl: '', From 14744acaede0776f77c91fe7dc8786c836f0fcc1 Mon Sep 17 00:00:00 2001 From: silverwind Date: Sat, 14 Mar 2026 01:32:01 +0100 Subject: [PATCH 024/102] Update stale webpack references to vite Co-Authored-By: Claude (Opus 4.6) --- Dockerfile | 2 +- Dockerfile.rootless | 2 +- vite.config.ts | 3 ++- web_src/js/webcomponents/README.md | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index 9922cee9c412e..67886efa10dfa 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # syntax=docker/dockerfile:1 -# Build frontend on the native platform to avoid QEMU-related issues with esbuild/webpack +# Build frontend on the native platform to avoid QEMU-related issues with vite FROM --platform=$BUILDPLATFORM docker.io/library/golang:1.26-alpine3.23 AS frontend-build RUN apk --no-cache add build-base git nodejs pnpm WORKDIR /src diff --git a/Dockerfile.rootless b/Dockerfile.rootless index a1742e3d51f13..a101b977bc657 100644 --- a/Dockerfile.rootless +++ b/Dockerfile.rootless @@ -1,5 +1,5 @@ # syntax=docker/dockerfile:1 -# Build frontend on the native platform to avoid QEMU-related issues with esbuild/webpack +# Build frontend on the native platform to avoid QEMU-related issues with vite FROM --platform=$BUILDPLATFORM docker.io/library/golang:1.26-alpine3.23 AS frontend-build RUN apk --no-cache add build-base git nodejs pnpm WORKDIR /src diff --git a/vite.config.ts b/vite.config.ts index c7068f61dfe9a..1b3f33875c733 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,4 +1,4 @@ -import {build, defineConfig, type InlineConfig, type Manifest, type Plugin} from 'vite'; +import {build, defineConfig} from 'vite'; import vuePlugin from '@vitejs/plugin-vue'; import {stringPlugin} from 'vite-string-plugin'; import {readFileSync, writeFileSync, unlinkSync, globSync} from 'node:fs'; @@ -9,6 +9,7 @@ import tailwindcss from 'tailwindcss'; import tailwindConfig from './tailwind.config.ts'; import wrapAnsi from 'wrap-ansi'; import licensePlugin from 'rollup-plugin-license'; +import type {InlineConfig, Manifest, Plugin} from 'vite'; const isProduction = env.NODE_ENV !== 'development'; diff --git a/web_src/js/webcomponents/README.md b/web_src/js/webcomponents/README.md index 45af58e1d221d..5a688962680f9 100644 --- a/web_src/js/webcomponents/README.md +++ b/web_src/js/webcomponents/README.md @@ -8,4 +8,4 @@ https://developer.mozilla.org/en-US/docs/Web/Web_Components * These components are loaded in `` (before DOM body) in a separate entry point, they need to be lightweight to not affect the page loading time too much. * Do not import `svg.js` into a web component because that file is currently not tree-shakeable, import svg files individually insteat. -* All our components must be added to `webpack.config.js` so they work correctly in Vue. +* All our components must be added to `vite.config.ts` so they work correctly in Vue. From ef8811fe9ed0cc6c803d54342a6ecca4df1be188 Mon Sep 17 00:00:00 2001 From: silverwind Date: Sat, 14 Mar 2026 01:40:40 +0100 Subject: [PATCH 025/102] forbid jquery imports because of single global instance --- eslint.config.ts | 2 +- web_src/js/globals.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/eslint.config.ts b/eslint.config.ts index 8d0f9ed342874..a547607e47440 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -568,7 +568,7 @@ export default defineConfig([ 'no-restricted-exports': [0], 'no-restricted-globals': [2, ...restrictedGlobals], 'no-restricted-properties': [2, ...restrictedProperties], - 'no-restricted-imports': [0], + 'no-restricted-imports': [2, {paths: [{name: 'jquery', message: 'Use the global $ instead', allowTypeImports: true}]}], 'no-restricted-syntax': [2, 'WithStatement', 'ForInStatement', 'LabeledStatement', 'SequenceExpression'], 'no-return-assign': [0], 'no-script-url': [2], diff --git a/web_src/js/globals.ts b/web_src/js/globals.ts index 955515d25021c..d7fe02fdee471 100644 --- a/web_src/js/globals.ts +++ b/web_src/js/globals.ts @@ -1,2 +1,2 @@ -import jquery from 'jquery'; +import jquery from 'jquery'; // eslint-disable-line no-restricted-imports window.$ = window.jQuery = jquery; // only for Fomantic UI From fee04f30a6af09095fedd4330c1c444d108ca75b Mon Sep 17 00:00:00 2001 From: silverwind Date: Sat, 14 Mar 2026 02:24:53 +0100 Subject: [PATCH 026/102] Suppress plugin timing warnings for worker builds Co-Authored-By: Claude (Opus 4.6) --- vite.config.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/vite.config.ts b/vite.config.ts index 1b3f33875c733..5312e00421f4b 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -203,6 +203,9 @@ export default defineConfig(commonViteOpts({ sourceMaps === 'reduced' && reducedSourcemapPlugin(), ], rolldownOptions: { + checks: { + pluginTimings: false, + }, output: { entryFileNames: 'js/[name].[hash:8].js', }, From 93ce326abbd046096d2e53d5992fb9e1f14d9a74 Mon Sep 17 00:00:00 2001 From: silverwind Date: Sat, 14 Mar 2026 02:33:05 +0100 Subject: [PATCH 027/102] Extract shared rolldownOptions for main, IIFE and worker builds Co-Authored-By: Claude (Opus 4.6) --- vite.config.ts | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/vite.config.ts b/vite.config.ts index 5312e00421f4b..e550787b1e6d7 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -9,7 +9,7 @@ import tailwindcss from 'tailwindcss'; import tailwindConfig from './tailwind.config.ts'; import wrapAnsi from 'wrap-ansi'; import licensePlugin from 'rollup-plugin-license'; -import type {InlineConfig, Manifest, Plugin} from 'vite'; +import type {InlineConfig, Manifest, Plugin, Rolldown} from 'vite'; const isProduction = env.NODE_ENV !== 'development'; @@ -43,8 +43,15 @@ const webComponents = new Set([ const formatLicenseText = (licenseText: string) => wrapAnsi(licenseText || '', 80).trim(); -function commonViteOpts({build, ...rest}: T): T { - const {rolldownOptions, ...buildRest} = build || {}; +const commonRolldownOptions: Rolldown.RolldownOptions = { + checks: { + eval: false, // htmx needs eval + pluginTimings: false, + }, +}; + +function commonViteOpts({build, ...other}: T): T { + const {rolldownOptions, ...otherBuild} = build || {}; return { configFile: false, root: import.meta.dirname, @@ -58,15 +65,12 @@ function commonViteOpts({build, ...rest}: T): T { cssMinify: 'esbuild', reportCompressedSize: false, rolldownOptions: { - checks: { - eval: false, // htmx needs eval - pluginTimings: false, - }, + ...commonRolldownOptions, ...rolldownOptions, }, - ...buildRest, + ...otherBuild, }, - ...rest, + ...other, } as InlineConfig & T; } @@ -203,9 +207,7 @@ export default defineConfig(commonViteOpts({ sourceMaps === 'reduced' && reducedSourcemapPlugin(), ], rolldownOptions: { - checks: { - pluginTimings: false, - }, + ...commonRolldownOptions, output: { entryFileNames: 'js/[name].[hash:8].js', }, From ae247851f4da9f3b47f983b2bf0b405d776df218 Mon Sep 17 00:00:00 2001 From: silverwind Date: Sat, 14 Mar 2026 02:45:04 +0100 Subject: [PATCH 028/102] use function --- vite.config.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/vite.config.ts b/vite.config.ts index e550787b1e6d7..1f5e1e92d3f0c 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -41,7 +41,9 @@ const webComponents = new Set([ 'text-expander', ]); -const formatLicenseText = (licenseText: string) => wrapAnsi(licenseText || '', 80).trim(); +function formatLicenseText(licenseText: string) { + return wrapAnsi(licenseText || '', 80).trim(); +} const commonRolldownOptions: Rolldown.RolldownOptions = { checks: { From b5d5e46be1873f255417268dc703eff365e4637a Mon Sep 17 00:00:00 2001 From: silverwind Date: Sat, 14 Mar 2026 03:08:17 +0100 Subject: [PATCH 029/102] Remove reduced sourcemap mode, simplify to true/false MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The reduced mode (stripping node_modules from sourcemaps) added build complexity without practical benefit — Error.stack always shows minified positions regardless of sourcemaps. Sourcemaps are now either fully enabled (dev default) or fully disabled (prod default). Co-Authored-By: Claude (Opus 4.6) --- vite.config.ts | 50 ++------------------------------------------------ 1 file changed, 2 insertions(+), 48 deletions(-) diff --git a/vite.config.ts b/vite.config.ts index 1f5e1e92d3f0c..4ee60425fc435 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -13,16 +13,7 @@ import type {InlineConfig, Manifest, Plugin, Rolldown} from 'vite'; const isProduction = env.NODE_ENV !== 'development'; -// ENABLE_SOURCEMAP accepts the following values: -// true - all enabled, the default in development -// reduced - minimal sourcemaps, the default in production -// false - all disabled -let sourceMaps: string | undefined; -if ('ENABLE_SOURCEMAP' in env) { - sourceMaps = ['true', 'false'].includes(env.ENABLE_SOURCEMAP || '') ? env.ENABLE_SOURCEMAP : 'reduced'; -} else { - sourceMaps = isProduction ? 'reduced' : 'true'; -} +const enableSourcemap = env.ENABLE_SOURCEMAP === 'false' ? false : env.ENABLE_SOURCEMAP === 'true' ? true : !isProduction; const outDir = fileURLToPath(new URL('public/assets', import.meta.url)); @@ -61,7 +52,7 @@ function commonViteOpts({build, ...other}: T): T { build: { outDir, emptyOutDir: false, - sourcemap: sourceMaps !== 'false', + sourcemap: enableSourcemap, target: 'es2020', minify: isProduction, cssMinify: 'esbuild', @@ -103,7 +94,6 @@ function iifeIndexPlugin(): Plugin { }, plugins: [ stringPlugin(), - sourceMaps === 'reduced' && reducedSourcemapPlugin(), ], })); @@ -129,38 +119,6 @@ function iifeIndexPlugin(): Plugin { }; } -// In 'reduced' mode, exclude node_modules from sourcemaps -function reducedSourcemapPlugin(runCloseBundle = false): Plugin { - return { - name: 'reduced-sourcemap', - enforce: 'post', - transform(code, id) { - if (id.includes('node_modules')) { - return {code, map: {mappings: ''}}; - } - return null; - }, - // Delete map files with no own code, strip node_modules sourcesContent from the rest. - // Rolldown ignores generateBundle mutations, so we must rewrite files in closeBundle. - ...(runCloseBundle && {closeBundle() { - for (const file of globSync('**/*.map', {cwd: outDir})) { - const mapPath = join(outDir, file); - const map = JSON.parse(readFileSync(mapPath, 'utf8')); - const hasOwnCode = map.sources?.some((s: string) => !s.includes('node_modules')); - if (!hasOwnCode) { - unlinkSync(mapPath); - continue; - } - if (!map.sourcesContent?.length) continue; - map.sourcesContent = map.sourcesContent.map((content: string, i: number) => - map.sources[i]?.includes('node_modules') ? '' : content, - ); - writeFileSync(mapPath, JSON.stringify(map)); - } - }}), - }; -} - // Filter out legacy font formats from CSS, keeping only woff2 function filterCssUrlPlugin(): Plugin { return { @@ -205,9 +163,6 @@ export default defineConfig(commonViteOpts({ }, }, worker: { - plugins: () => [ - sourceMaps === 'reduced' && reducedSourcemapPlugin(), - ], rolldownOptions: { ...commonRolldownOptions, output: { @@ -240,7 +195,6 @@ export default defineConfig(commonViteOpts({ iifeIndexPlugin(), filterCssUrlPlugin(), stringPlugin(), - sourceMaps === 'reduced' && reducedSourcemapPlugin(true), vuePlugin({ template: { compilerOptions: { From bc05ab56326e961426b87642e3128fdd65fe2a4c Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sun, 15 Mar 2026 16:10:47 +0800 Subject: [PATCH 030/102] fix comment --- Dockerfile | 2 +- Dockerfile.rootless | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 67886efa10dfa..323f06125fef8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # syntax=docker/dockerfile:1 -# Build frontend on the native platform to avoid QEMU-related issues with vite +# Build frontend on the native platform to avoid QEMU-related issues with nodejs ecosystem FROM --platform=$BUILDPLATFORM docker.io/library/golang:1.26-alpine3.23 AS frontend-build RUN apk --no-cache add build-base git nodejs pnpm WORKDIR /src diff --git a/Dockerfile.rootless b/Dockerfile.rootless index a101b977bc657..83c69cbd513b4 100644 --- a/Dockerfile.rootless +++ b/Dockerfile.rootless @@ -1,5 +1,5 @@ # syntax=docker/dockerfile:1 -# Build frontend on the native platform to avoid QEMU-related issues with vite +# Build frontend on the native platform to avoid QEMU-related issues with nodejs ecosystem FROM --platform=$BUILDPLATFORM docker.io/library/golang:1.26-alpine3.23 AS frontend-build RUN apk --no-cache add build-base git nodejs pnpm WORKDIR /src From 467dde9ef50c0fcc015e94c302e8779fe1ea626f Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sun, 15 Mar 2026 16:31:54 +0800 Subject: [PATCH 031/102] add jQuery check to devtest page --- templates/devtest/devtest-header.tmpl | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/templates/devtest/devtest-header.tmpl b/templates/devtest/devtest-header.tmpl index e55c51076f554..3f5f6e5ab7492 100644 --- a/templates/devtest/devtest-header.tmpl +++ b/templates/devtest/devtest-header.tmpl @@ -1,3 +1,8 @@ {{template "base/head" ctx.RootData}} + {{template "base/alert" .}} From 6ae005d71ad5f5a813227b0d65d7d224c9c42fd9 Mon Sep 17 00:00:00 2001 From: silverwind Date: Sun, 15 Mar 2026 10:22:11 +0100 Subject: [PATCH 032/102] Use atomic.Pointer for manifest state to fix data race in dev mode Co-Authored-By: Claude (Opus 4.6) --- modules/public/manifest.go | 29 +++++++++++++++++++---------- modules/public/manifest_test.go | 6 +++--- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/modules/public/manifest.go b/modules/public/manifest.go index caa9dcac7ad9c..87036641a1ee2 100644 --- a/modules/public/manifest.go +++ b/modules/public/manifest.go @@ -7,6 +7,7 @@ import ( "io" "path" "sync" + "sync/atomic" "code.gitea.io/gitea/modules/assetfs" "code.gitea.io/gitea/modules/json" @@ -21,11 +22,15 @@ type viteManifestEntry struct { CSS []string `json:"css"` } +type manifestState struct { + paths map[string]string + modTime int64 +} + var ( - manifestOnce sync.Once - manifestFS *assetfs.LayeredFS - manifestPaths map[string]string - manifestModTime int64 + manifestOnce sync.Once + manifestFS *assetfs.LayeredFS + manifestData atomic.Pointer[manifestState] ) const manifestPath = "assets/.vite/manifest.json" @@ -65,29 +70,32 @@ func reloadManifest() { f, err := manifestFS.Open(manifestPath) if err != nil { log.Error("Failed to open Vite manifest: %v", err) - manifestPaths = make(map[string]string) + manifestData.Store(&manifestState{paths: make(map[string]string)}) return } defer f.Close() + var modTime int64 fi, err := f.Stat() if err == nil { - manifestModTime = fi.ModTime().UnixNano() + modTime = fi.ModTime().UnixNano() } data, err := io.ReadAll(f) if err != nil { log.Error("Failed to read Vite manifest: %v", err) - manifestPaths = make(map[string]string) + manifestData.Store(&manifestState{paths: make(map[string]string)}) return } - manifestPaths = parseManifest(data) + manifestData.Store(&manifestState{paths: parseManifest(data), modTime: modTime}) } func getManifestPaths() map[string]string { manifestOnce.Do(initManifest) + state := 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 !setting.IsProd { @@ -95,12 +103,13 @@ func getManifestPaths() map[string]string { if err == nil { fi, err := f.Stat() f.Close() - if err == nil && fi.ModTime().UnixNano() != manifestModTime { + if err == nil && fi.ModTime().UnixNano() != state.modTime { reloadManifest() + state = manifestData.Load() } } } - return manifestPaths + return state.paths } // GetAssetPath resolves an unhashed asset path to its content-hashed path from the Vite manifest. diff --git a/modules/public/manifest_test.go b/modules/public/manifest_test.go index 0f8af13cbb331..d00a90c24aaa4 100644 --- a/modules/public/manifest_test.go +++ b/modules/public/manifest_test.go @@ -63,9 +63,9 @@ func TestParseManifest(t *testing.T) { func TestGetAssetPathFallback(t *testing.T) { // When manifest is not loaded, GetAssetPath should return the input as-is - old := manifestPaths - manifestPaths = make(map[string]string) - defer func() { manifestPaths = old }() + old := manifestData.Load() + manifestData.Store(&manifestState{paths: make(map[string]string)}) + defer func() { manifestData.Store(old) }() assert.Equal(t, "js/index.js", GetAssetPath("js/index.js")) assert.Equal(t, "css/theme-gitea-dark.css", GetAssetPath("css/theme-gitea-dark.css")) From f3a8a190794663908073cf998edaf88903b226c6 Mon Sep 17 00:00:00 2001 From: silverwind Date: Sun, 15 Mar 2026 10:42:29 +0100 Subject: [PATCH 033/102] Move htmx to IIFE globals, forbid `htmx.org` imports Move htmx + idiomorph imports and config to globals.ts (IIFE), matching the jQuery pattern. Delete htmx.ts and remove initHtmx. Error event listeners stay in index-domready (need document.body + toast). Forbid importing 'htmx.org' and 'idiomorph/htmx' in ESLint (type imports allowed). Co-Authored-By: Claude (Opus 4.6) --- eslint.config.ts | 6 +++++- web_src/js/globals.ts | 10 +++++++++- web_src/js/htmx.ts | 26 -------------------------- web_src/js/index-domready.ts | 16 ++++++++++++++-- 4 files changed, 28 insertions(+), 30 deletions(-) delete mode 100644 web_src/js/htmx.ts diff --git a/eslint.config.ts b/eslint.config.ts index a547607e47440..3f3bcf9108f20 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -568,7 +568,11 @@ export default defineConfig([ 'no-restricted-exports': [0], 'no-restricted-globals': [2, ...restrictedGlobals], 'no-restricted-properties': [2, ...restrictedProperties], - 'no-restricted-imports': [2, {paths: [{name: 'jquery', message: 'Use the global $ instead', allowTypeImports: true}]}], + 'no-restricted-imports': [2, {paths: [ + {name: 'jquery', message: 'Use the global $ instead', allowTypeImports: true}, + {name: 'htmx.org', message: 'Use the global htmx instead', allowTypeImports: true}, + {name: 'idiomorph/htmx', message: 'Loaded in globals.ts'}, + ]}], 'no-restricted-syntax': [2, 'WithStatement', 'ForInStatement', 'LabeledStatement', 'SequenceExpression'], 'no-return-assign': [0], 'no-script-url': [2], diff --git a/web_src/js/globals.ts b/web_src/js/globals.ts index d7fe02fdee471..321a48d9dc6b8 100644 --- a/web_src/js/globals.ts +++ b/web_src/js/globals.ts @@ -1,2 +1,10 @@ import jquery from 'jquery'; // eslint-disable-line no-restricted-imports -window.$ = window.jQuery = jquery; // only for Fomantic UI +import htmx from 'htmx.org'; // eslint-disable-line no-restricted-imports +import 'idiomorph/htmx'; // eslint-disable-line no-restricted-imports + +window.$ = window.jQuery = jquery; +window.htmx = htmx; + +// https://htmx.org/reference/#config +htmx.config.requestClass = 'is-loading'; +htmx.config.scrollIntoViewOnBoost = false; diff --git a/web_src/js/htmx.ts b/web_src/js/htmx.ts deleted file mode 100644 index acc3df1d81d79..0000000000000 --- a/web_src/js/htmx.ts +++ /dev/null @@ -1,26 +0,0 @@ -import htmx from 'htmx.org'; -import 'idiomorph/htmx'; -import type {HtmxResponseInfo} from 'htmx.org'; -import {showErrorToast} from './modules/toast.ts'; - -type HtmxEvent = Event & {detail: HtmxResponseInfo}; - -export function initHtmx() { - window.htmx = htmx; - - // https://htmx.org/reference/#config - htmx.config.requestClass = 'is-loading'; - htmx.config.scrollIntoViewOnBoost = false; - - // https://htmx.org/events/#htmx:sendError - document.body.addEventListener('htmx:sendError', (event: Partial) => { - // TODO: add translations - showErrorToast(`Network error when calling ${event.detail!.requestConfig.path}`); - }); - - // https://htmx.org/events/#htmx:responseError - document.body.addEventListener('htmx:responseError', (event: Partial) => { - // TODO: add translations - showErrorToast(`Error ${event.detail!.xhr.status} when calling ${event.detail!.requestConfig.path}`); - }); -} diff --git a/web_src/js/index-domready.ts b/web_src/js/index-domready.ts index 0c8f90a17b039..bf5915ecb6913 100644 --- a/web_src/js/index-domready.ts +++ b/web_src/js/index-domready.ts @@ -1,8 +1,9 @@ import '../fomantic/build/fomantic.js'; import '../fomantic/build/fomantic.css'; import '../css/index.css'; +import type {HtmxResponseInfo} from 'htmx.org'; +import {showErrorToast} from './modules/toast.ts'; -import {initHtmx} from './htmx.ts'; import {initDashboardRepoList} from './features/dashboard.ts'; import {initGlobalCopyToClipboardListener} from './features/clipboard.ts'; import {initRepoGraphGit} from './features/repo-graph.ts'; @@ -69,7 +70,6 @@ import {initGlobalShortcut} from './modules/shortcut.ts'; const initStartTime = performance.now(); const initPerformanceTracer = callInitFunctions([ - initHtmx, initSubmitEventPolyfill, initGiteaFomantic, @@ -172,4 +172,16 @@ if (initDur > 500) { console.error(`slow init functions took ${initDur.toFixed(3)}ms`); } +// https://htmx.org/events/#htmx:sendError +type HtmxEvent = Event & {detail: HtmxResponseInfo}; +document.body.addEventListener('htmx:sendError', (event) => { + // TODO: add translations + showErrorToast(`Network error when calling ${(event as HtmxEvent).detail.requestConfig.path}`); +}); +// https://htmx.org/events/#htmx:responseError +document.body.addEventListener('htmx:responseError', (event) => { + // TODO: add translations + showErrorToast(`Error ${(event as HtmxEvent).detail.xhr.status} when calling ${(event as HtmxEvent).detail.requestConfig.path}`); +}); + document.dispatchEvent(new CustomEvent('gitea:index-ready')); From e66d7b41b70cc1cbbf56d3aad7f6d45d54512f64 Mon Sep 17 00:00:00 2001 From: silverwind Date: Sun, 15 Mar 2026 10:53:33 +0100 Subject: [PATCH 034/102] eslint tweaks --- eslint.config.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/eslint.config.ts b/eslint.config.ts index 3f3bcf9108f20..e6e8299b0a003 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -571,7 +571,7 @@ export default defineConfig([ 'no-restricted-imports': [2, {paths: [ {name: 'jquery', message: 'Use the global $ instead', allowTypeImports: true}, {name: 'htmx.org', message: 'Use the global htmx instead', allowTypeImports: true}, - {name: 'idiomorph/htmx', message: 'Loaded in globals.ts'}, + {name: 'idiomorph/htmx', message: 'Loaded in globals.ts', allowTypeImports: true}, ]}], 'no-restricted-syntax': [2, 'WithStatement', 'ForInStatement', 'LabeledStatement', 'SequenceExpression'], 'no-return-assign': [0], @@ -1011,6 +1011,6 @@ export default defineConfig([ }, { files: ['web_src/**/*'], - languageOptions: {globals: {...globals.browser, ...globals.jquery}}, + languageOptions: {globals: {...globals.browser, ...globals.jquery, htmx: false}}, }, ]); From f0cf85da2c34db5ca9285196c5ce9a8f6fc49cc7 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sun, 15 Mar 2026 18:14:35 +0800 Subject: [PATCH 035/102] C O M M E N T --- web_src/js/globals.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/web_src/js/globals.ts b/web_src/js/globals.ts index 321a48d9dc6b8..9cd66d8322b64 100644 --- a/web_src/js/globals.ts +++ b/web_src/js/globals.ts @@ -2,7 +2,13 @@ import jquery from 'jquery'; // eslint-disable-line no-restricted-imports import htmx from 'htmx.org'; // eslint-disable-line no-restricted-imports import 'idiomorph/htmx'; // eslint-disable-line no-restricted-imports +// Some users still use inline scripts and expect jQuery to be available globally. +// To avoid breaking existing users and custom plugins, import jQuery globally without ES module. window.$ = window.jQuery = jquery; + +// There is a bug in htmx, it incorrectly checks "readyState === 'complete'" when the DOM tree is ready and won't trigger DOMContentLoaded +// The bug makes htmx impossible to be loaded from an ES module: importing the htmx in onDomReady will make htmx skip its initialization. +// ref: https://github.com/bigskysoftware/htmx/pull/3365 window.htmx = htmx; // https://htmx.org/reference/#config From be0892627b7b229b813506df3bd4a315a522360b Mon Sep 17 00:00:00 2001 From: silverwind Date: Sun, 15 Mar 2026 11:28:12 +0100 Subject: [PATCH 036/102] Stub XPathEvaluator in vitest setup for htmx compatibility happy-dom does not implement XPathEvaluator which htmx uses at module evaluation time. Use dynamic import for globals.ts so the stub is applied before htmx evaluates. Co-Authored-By: Claude (Opus 4.6) --- web_src/js/vitest.setup.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/web_src/js/vitest.setup.ts b/web_src/js/vitest.setup.ts index 099a90158bfda..b213e49abd476 100644 --- a/web_src/js/vitest.setup.ts +++ b/web_src/js/vitest.setup.ts @@ -1,4 +1,13 @@ -import './globals.ts'; +// Stub APIs not implemented by happy-dom but needed by dependencies +// XPathEvaluator is used by htmx at module evaluation time +if (!globalThis.XPathEvaluator) { + globalThis.XPathEvaluator = class { + createExpression() { return {evaluate: () => ({iterateNext: () => null})}; } + } as any; +} + +// Dynamic import so polyfills above are applied before htmx evaluates +await import('./globals.ts'); window.config = { appUrl: 'http://localhost:3000/', From 1487b7a01d2100e7473cd491d2a220accbd1225f Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sun, 15 Mar 2026 18:30:22 +0800 Subject: [PATCH 037/102] fix parseManifest --- modules/public/manifest.go | 92 +++++++++++++++++---------------- modules/public/manifest_test.go | 2 +- 2 files changed, 49 insertions(+), 45 deletions(-) diff --git a/modules/public/manifest.go b/modules/public/manifest.go index 87036641a1ee2..169a25915b57d 100644 --- a/modules/public/manifest.go +++ b/modules/public/manifest.go @@ -8,38 +8,38 @@ import ( "path" "sync" "sync/atomic" + "time" - "code.gitea.io/gitea/modules/assetfs" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" ) -type viteManifestEntry struct { +type manifestEntry struct { File string `json:"file"` Name string `json:"name"` IsEntry bool `json:"isEntry"` CSS []string `json:"css"` } -type manifestState struct { - paths map[string]string - modTime int64 +type manifestDataStruct struct { + paths map[string]string + modTime int64 + checkTime time.Time } var ( - manifestOnce sync.Once - manifestFS *assetfs.LayeredFS - manifestData atomic.Pointer[manifestState] + manifestData atomic.Pointer[manifestDataStruct] + manifestFS = sync.OnceValue(AssetFS) ) const manifestPath = "assets/.vite/manifest.json" func parseManifest(data []byte) map[string]string { - var manifest map[string]viteManifestEntry + var manifest map[string]manifestEntry if err := json.Unmarshal(data, &manifest); err != nil { - log.Error("Failed to parse Vite manifest: %v", err) - return make(map[string]string) + log.Error("Failed to parse frontend manifest: %v", err) + return nil } paths := make(map[string]string) @@ -61,58 +61,62 @@ func parseManifest(data []byte) map[string]string { return paths } -func initManifest() { - manifestFS = AssetFS() - reloadManifest() -} +func reloadManifest() *manifestDataStruct { + now := time.Now() + data := manifestData.Load() + if data != nil && now.Sub(data.checkTime) < time.Second { + // a single request triggers multiple calls to GetAssetPath + // do not check the manifest file too frequently + return data + } -func reloadManifest() { - f, err := manifestFS.Open(manifestPath) + f, err := manifestFS().Open(manifestPath) if err != nil { - log.Error("Failed to open Vite manifest: %v", err) - manifestData.Store(&manifestState{paths: make(map[string]string)}) - return + log.Error("Failed to open frontend manifest: %v", err) + return data } defer f.Close() - var modTime int64 fi, err := f.Stat() - if err == nil { - modTime = fi.ModTime().UnixNano() + if err != nil { + log.Error("Failed to stat frontend manifest: %v", err) + return data } - data, err := io.ReadAll(f) - if err != nil { - log.Error("Failed to read Vite manifest: %v", err) - manifestData.Store(&manifestState{paths: make(map[string]string)}) - return + needReload := data == nil || fi.ModTime().UnixNano() != data.modTime + if !needReload { + return data } - manifestData.Store(&manifestState{paths: parseManifest(data), modTime: modTime}) + manifestContent, err := io.ReadAll(f) + if err != nil { + log.Error("Failed to read frontend manifest: %v", err) + return data + } + data = &manifestDataStruct{ + paths: parseManifest(manifestContent), + modTime: fi.ModTime().UnixNano(), + checkTime: now, + } + manifestData.Store(data) + return data } func getManifestPaths() map[string]string { - manifestOnce.Do(initManifest) - - state := manifestData.Load() + 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 !setting.IsProd { - f, err := manifestFS.Open(manifestPath) - if err == nil { - fi, err := f.Stat() - f.Close() - if err == nil && fi.ModTime().UnixNano() != state.modTime { - reloadManifest() - state = manifestData.Load() - } - } + if data == nil || !setting.IsProd { + data = reloadManifest() + } + if data != nil { + return data.paths } - return state.paths + return nil } -// GetAssetPath resolves an unhashed asset path to its content-hashed path from the Vite manifest. +// GetAssetPath resolves an unhashed asset path to its content-hashed path from the frontend 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 { diff --git a/modules/public/manifest_test.go b/modules/public/manifest_test.go index d00a90c24aaa4..50f7e0df7e093 100644 --- a/modules/public/manifest_test.go +++ b/modules/public/manifest_test.go @@ -64,7 +64,7 @@ func TestParseManifest(t *testing.T) { func TestGetAssetPathFallback(t *testing.T) { // When manifest is not loaded, GetAssetPath should return the input as-is old := manifestData.Load() - manifestData.Store(&manifestState{paths: make(map[string]string)}) + manifestData.Store(&manifestDataStruct{paths: make(map[string]string)}) defer func() { manifestData.Store(old) }() assert.Equal(t, "js/index.js", GetAssetPath("js/index.js")) From 1efff9403797bb106bc9fff9e69d5d36060a23a7 Mon Sep 17 00:00:00 2001 From: silverwind Date: Sun, 15 Mar 2026 11:33:25 +0100 Subject: [PATCH 038/102] Stub XPathEvaluator in vitest setup for htmx compatibility happy-dom does not implement XPathEvaluator which htmx uses at module evaluation time. Use dynamic import for globals.ts so the stub is applied before htmx evaluates. Co-Authored-By: Claude (Opus 4.6) --- web_src/js/vitest.setup.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/web_src/js/vitest.setup.ts b/web_src/js/vitest.setup.ts index b213e49abd476..103bdb99a4524 100644 --- a/web_src/js/vitest.setup.ts +++ b/web_src/js/vitest.setup.ts @@ -2,7 +2,7 @@ // XPathEvaluator is used by htmx at module evaluation time if (!globalThis.XPathEvaluator) { globalThis.XPathEvaluator = class { - createExpression() { return {evaluate: () => ({iterateNext: () => null})}; } + createExpression() { return {evaluate: () => ({iterateNext: () => null})} } } as any; } @@ -22,3 +22,5 @@ window.config = { mermaidMaxSourceCharacters: 5000, i18n: {}, }; + +export {}; // mark as module for top-level await From 412ba3c6fe54c99396d59f6377efffcea1d6652a Mon Sep 17 00:00:00 2001 From: silverwind Date: Sun, 15 Mar 2026 12:42:31 +0100 Subject: [PATCH 039/102] Merge tiny mermaid parser chunks via `manualChunks` Group all @mermaid-js/parser modules into a single chunk since mermaid-parser.core loads them all together anyway. Reduces chunk count by ~24 files. Co-Authored-By: Claude (Opus 4.6) --- vite.config.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/vite.config.ts b/vite.config.ts index 4ee60425fc435..362296c4d53e8 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -149,6 +149,10 @@ export default defineConfig(commonViteOpts({ ...themes, }, output: { + // Merge tiny mermaid parser chunks that are always loaded together + manualChunks(id) { + if (id.includes('node_modules') && id.includes('@mermaid-js/parser')) return 'mermaid-parser'; + }, entryFileNames: 'js/[name].[hash:8].js', chunkFileNames: 'js/[name].[hash:8].js', assetFileNames: (info: {name?: string}) => { From 303328bca32b3f07f10982f03e676c3496de15cb Mon Sep 17 00:00:00 2001 From: silverwind Date: Sun, 15 Mar 2026 12:48:01 +0100 Subject: [PATCH 040/102] fix lint --- vite.config.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/vite.config.ts b/vite.config.ts index 362296c4d53e8..3bc1604527400 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -149,9 +149,10 @@ export default defineConfig(commonViteOpts({ ...themes, }, output: { - // Merge tiny mermaid parser chunks that are always loaded together manualChunks(id) { + // Merge tiny mermaid parser chunks that are always loaded together if (id.includes('node_modules') && id.includes('@mermaid-js/parser')) return 'mermaid-parser'; + return undefined; }, entryFileNames: 'js/[name].[hash:8].js', chunkFileNames: 'js/[name].[hash:8].js', From 18070c4d059134c1f9105c818822343745a394cd Mon Sep 17 00:00:00 2001 From: silverwind Date: Sun, 15 Mar 2026 13:43:28 +0100 Subject: [PATCH 041/102] Use `codeSplitting` instead of deprecated `manualChunks` Migrate to Rolldown's codeSplitting API which replaces the deprecated manualChunks option. Add vue-runtime group to merge Vue runtime chunks. Co-Authored-By: Claude (Opus 4.6) --- vite.config.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/vite.config.ts b/vite.config.ts index 3bc1604527400..5c52aa98e8653 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -149,10 +149,11 @@ export default defineConfig(commonViteOpts({ ...themes, }, output: { - manualChunks(id) { - // Merge tiny mermaid parser chunks that are always loaded together - if (id.includes('node_modules') && id.includes('@mermaid-js/parser')) return 'mermaid-parser'; - return undefined; + codeSplitting: { + groups: [ + {name: 'mermaid-parser', test: '@mermaid-js/parser', priority: 10}, + {name: 'vue-runtime', test: /[\\/]@vue[\\/]runtime/, priority: 5}, + ], }, entryFileNames: 'js/[name].[hash:8].js', chunkFileNames: 'js/[name].[hash:8].js', From c0984991a9f3b0d6b3408d301dd763531a285963 Mon Sep 17 00:00:00 2001 From: silverwind Date: Sun, 15 Mar 2026 13:54:12 +0100 Subject: [PATCH 042/102] Merge mermaid diagram chunks into single `mermaid-core` chunk Reduces JS chunk count from 199 to 162 by merging all mermaid diagram types and shared internals into one chunk. Co-Authored-By: Claude (Opus 4.6) --- vite.config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/vite.config.ts b/vite.config.ts index 5c52aa98e8653..d18bff71a1e40 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -152,6 +152,7 @@ export default defineConfig(commonViteOpts({ codeSplitting: { groups: [ {name: 'mermaid-parser', test: '@mermaid-js/parser', priority: 10}, + {name: 'mermaid-core', test: /[\\/]mermaid[\\/]dist[\\/]/, minSize: 0, priority: 5}, {name: 'vue-runtime', test: /[\\/]@vue[\\/]runtime/, priority: 5}, ], }, From 9ff878994a4951d379ecb328384b6c9304bf1471 Mon Sep 17 00:00:00 2001 From: silverwind Date: Sun, 15 Mar 2026 14:02:07 +0100 Subject: [PATCH 043/102] Remove redundant `minSize: 0` from mermaid-core group Co-Authored-By: Claude (Opus 4.6) --- vite.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vite.config.ts b/vite.config.ts index d18bff71a1e40..498cd36bbda20 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -152,7 +152,7 @@ export default defineConfig(commonViteOpts({ codeSplitting: { groups: [ {name: 'mermaid-parser', test: '@mermaid-js/parser', priority: 10}, - {name: 'mermaid-core', test: /[\\/]mermaid[\\/]dist[\\/]/, minSize: 0, priority: 5}, + {name: 'mermaid-core', test: /[\\/]mermaid[\\/]dist[\\/]/, priority: 5}, {name: 'vue-runtime', test: /[\\/]@vue[\\/]runtime/, priority: 5}, ], }, From 4c5b77c8f1bdbd765454f6f5cf6a4f83e5bcaec5 Mon Sep 17 00:00:00 2001 From: silverwind Date: Sun, 15 Mar 2026 14:03:41 +0100 Subject: [PATCH 044/102] Remove unnecessary priority from vue-runtime group Co-Authored-By: Claude (Opus 4.6) --- vite.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vite.config.ts b/vite.config.ts index 498cd36bbda20..db640dc44784f 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -153,7 +153,7 @@ export default defineConfig(commonViteOpts({ groups: [ {name: 'mermaid-parser', test: '@mermaid-js/parser', priority: 10}, {name: 'mermaid-core', test: /[\\/]mermaid[\\/]dist[\\/]/, priority: 5}, - {name: 'vue-runtime', test: /[\\/]@vue[\\/]runtime/, priority: 5}, + {name: 'vue-runtime', test: /[\\/]@vue[\\/]runtime/}, ], }, entryFileNames: 'js/[name].[hash:8].js', From c6f6ea83fc8e312f2e1eb8bc182e3e186c64fbea Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sun, 15 Mar 2026 21:00:02 +0800 Subject: [PATCH 045/102] add UnencryptedHTTP2 --- modules/graceful/server_http.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/modules/graceful/server_http.go b/modules/graceful/server_http.go index 7c855ac64ec16..8bc9557c562f5 100644 --- a/modules/graceful/server_http.go +++ b/modules/graceful/server_http.go @@ -12,7 +12,12 @@ import ( func newHTTPServer(network, address, name string, handler http.Handler) (*Server, ServeFunction) { server := NewServer(network, address, name) + protocols := http.Protocols{} + protocols.SetHTTP1(true) + protocols.SetHTTP2(true) // HTTP/2 can only be used when Gitea is configured to use TLS + protocols.SetUnencryptedHTTP2(true) // Allow HTTP/2 without TLS, in case Gitea is behind a reverse proxy httpServer := http.Server{ + Protocols: &protocols, Handler: handler, BaseContext: func(net.Listener) context.Context { return GetManager().HammerContext() }, } From 204053585ad2bf918fbf059291493f4040797788 Mon Sep 17 00:00:00 2001 From: silverwind Date: Sun, 15 Mar 2026 14:38:16 +0100 Subject: [PATCH 046/102] Merge mermaid chunks without making them static imports Use includeDependenciesRecursively: false to prevent the mermaid codeSplitting group from absorbing shared dependencies like the preload-helper, which would make the mermaid chunk a static dependency of every page. Co-Authored-By: Claude (Opus 4.6) --- vite.config.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vite.config.ts b/vite.config.ts index db640dc44784f..0043a268b5c08 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -150,9 +150,9 @@ export default defineConfig(commonViteOpts({ }, output: { codeSplitting: { + includeDependenciesRecursively: false, groups: [ - {name: 'mermaid-parser', test: '@mermaid-js/parser', priority: 10}, - {name: 'mermaid-core', test: /[\\/]mermaid[\\/]dist[\\/]/, priority: 5}, + {name: 'mermaid', test: /[\\/]mermaid[\\/]|@mermaid-js[\\/]/, priority: 10}, {name: 'vue-runtime', test: /[\\/]@vue[\\/]runtime/}, ], }, From 592b67ba1e9c2651e676d139839efa68c336b821 Mon Sep 17 00:00:00 2001 From: silverwind Date: Sun, 15 Mar 2026 14:38:53 +0100 Subject: [PATCH 047/102] fmt --- modules/graceful/server_http.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/graceful/server_http.go b/modules/graceful/server_http.go index 8bc9557c562f5..77a2c3b6f837c 100644 --- a/modules/graceful/server_http.go +++ b/modules/graceful/server_http.go @@ -14,7 +14,7 @@ func newHTTPServer(network, address, name string, handler http.Handler) (*Server server := NewServer(network, address, name) protocols := http.Protocols{} protocols.SetHTTP1(true) - protocols.SetHTTP2(true) // HTTP/2 can only be used when Gitea is configured to use TLS + protocols.SetHTTP2(true) // HTTP/2 can only be used when Gitea is configured to use TLS protocols.SetUnencryptedHTTP2(true) // Allow HTTP/2 without TLS, in case Gitea is behind a reverse proxy httpServer := http.Server{ Protocols: &protocols, From e768b5c63fb34e92c211cf2a2e003307f20656d9 Mon Sep 17 00:00:00 2001 From: silverwind Date: Sun, 15 Mar 2026 14:41:45 +0100 Subject: [PATCH 048/102] comment --- vite.config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/vite.config.ts b/vite.config.ts index 0043a268b5c08..7361944978b5b 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -150,6 +150,7 @@ export default defineConfig(commonViteOpts({ }, output: { codeSplitting: { + // avoid absorbing shared deps into groups, which can make lazy chunks static includeDependenciesRecursively: false, groups: [ {name: 'mermaid', test: /[\\/]mermaid[\\/]|@mermaid-js[\\/]/, priority: 10}, From 89f8907579136c7eced9b4db8e3a87f78e4a1afe Mon Sep 17 00:00:00 2001 From: silverwind Date: Sun, 15 Mar 2026 14:44:57 +0100 Subject: [PATCH 049/102] Remove shared chunk dependencies from swagger entry Use native fetch in swagger instead of the shared fetch wrapper to eliminate utils and fetch shared chunks. Broaden vue group to include all @vue packages and rename to just "vue". Co-Authored-By: Claude (Opus 4.6) --- vite.config.ts | 2 +- web_src/js/standalone/swagger.ts | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/vite.config.ts b/vite.config.ts index 7361944978b5b..f98406657666b 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -154,7 +154,7 @@ export default defineConfig(commonViteOpts({ includeDependenciesRecursively: false, groups: [ {name: 'mermaid', test: /[\\/]mermaid[\\/]|@mermaid-js[\\/]/, priority: 10}, - {name: 'vue-runtime', test: /[\\/]@vue[\\/]runtime/}, + {name: 'vue', test: /[\\/]@vue[\\/]/}, ], }, entryFileNames: 'js/[name].[hash:8].js', diff --git a/web_src/js/standalone/swagger.ts b/web_src/js/standalone/swagger.ts index 6f7f0a65ee076..c7d66b781ece6 100644 --- a/web_src/js/standalone/swagger.ts +++ b/web_src/js/standalone/swagger.ts @@ -2,14 +2,13 @@ import '../../css/standalone/swagger.css'; import SwaggerUI from 'swagger-ui-dist/swagger-ui-es-bundle.js'; import 'swagger-ui-dist/swagger-ui.css'; import {load as loadYaml} from 'js-yaml'; -import {GET} from '../modules/fetch.ts'; window.addEventListener('load', async () => { const elSwaggerUi = document.querySelector('#swagger-ui')!; const url = elSwaggerUi.getAttribute('data-source')!; let spec: any; if (url) { - const res = await GET(url); + const res = await fetch(url); // eslint-disable-line no-restricted-globals spec = await res.json(); } else { const elSpecContent = elSwaggerUi.querySelector('.swagger-spec-content')!; From 1dacad8b8ca81c884c69ec980b5b26d5b6d0ba84 Mon Sep 17 00:00:00 2001 From: silverwind Date: Sun, 15 Mar 2026 16:06:27 +0100 Subject: [PATCH 050/102] Move relative-time to own Signed-off-by: silverwind --- vite.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vite.config.ts b/vite.config.ts index f98406657666b..0197e1d72ed55 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -26,9 +26,9 @@ const webComponents = new Set([ // our own, in web_src/js/webcomponents 'overflow-menu', 'origin-url', + 'relative-time', // from dependencies 'markdown-toolbar', - 'relative-time', 'text-expander', ]); From 93e92fa778433ce63698f3097c7bb2a3ffab81b0 Mon Sep 17 00:00:00 2001 From: silverwind Date: Sun, 15 Mar 2026 21:30:30 +0100 Subject: [PATCH 051/102] Clean up vite config - Remove unused fileURLToPath, use join(import.meta.dirname, ...) instead - Remove unnecessary process.env.NODE_ENV define from IIFE build - Remove dead webcomponents cleanup glob - Remove unnecessary try/catch around manifest parsing - Simplify enableSourcemap ternary - Simplify commonViteOpts generic signature - Simplify assetFileNames function - Use names instead of deprecated name in assetFileNames Co-Authored-By: Claude (Opus 4.6) --- vite.config.ts | 83 +++++++++++++++++++------------------------------- 1 file changed, 32 insertions(+), 51 deletions(-) diff --git a/vite.config.ts b/vite.config.ts index 0197e1d72ed55..e05e1259f0689 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -2,24 +2,21 @@ import {build, defineConfig} from 'vite'; import vuePlugin from '@vitejs/plugin-vue'; import {stringPlugin} from 'vite-string-plugin'; import {readFileSync, writeFileSync, unlinkSync, globSync} from 'node:fs'; -import {fileURLToPath} from 'node:url'; import {join, parse} from 'node:path'; import {env} from 'node:process'; import tailwindcss from 'tailwindcss'; import tailwindConfig from './tailwind.config.ts'; import wrapAnsi from 'wrap-ansi'; import licensePlugin from 'rollup-plugin-license'; -import type {InlineConfig, Manifest, Plugin, Rolldown} from 'vite'; +import type {InlineConfig, Plugin, Rolldown} from 'vite'; const isProduction = env.NODE_ENV !== 'development'; - -const enableSourcemap = env.ENABLE_SOURCEMAP === 'false' ? false : env.ENABLE_SOURCEMAP === 'true' ? true : !isProduction; - -const outDir = fileURLToPath(new URL('public/assets', import.meta.url)); +const enableSourcemap = env.ENABLE_SOURCEMAP ? env.ENABLE_SOURCEMAP === 'true' : !isProduction; +const outDir = join(import.meta.dirname, 'public/assets'); const themes: Record = {}; for (const path of globSync('web_src/css/themes/*.css', {cwd: import.meta.dirname})) { - themes[parse(path).name] = fileURLToPath(new URL(path, import.meta.url)); + themes[parse(path).name] = join(import.meta.dirname, path); } const webComponents = new Set([ @@ -43,7 +40,7 @@ const commonRolldownOptions: Rolldown.RolldownOptions = { }, }; -function commonViteOpts({build, ...other}: T): T { +function commonViteOpts({build, ...other}: InlineConfig): InlineConfig { const {rolldownOptions, ...otherBuild} = build || {}; return { configFile: false, @@ -55,7 +52,7 @@ function commonViteOpts({build, ...other}: T): T { sourcemap: enableSourcemap, target: 'es2020', minify: isProduction, - cssMinify: 'esbuild', + cssMinify: isProduction ? 'esbuild' : false, reportCompressedSize: false, rolldownOptions: { ...commonRolldownOptions, @@ -64,7 +61,7 @@ function commonViteOpts({build, ...other}: T): T { ...otherBuild, }, ...other, - } as InlineConfig & T; + }; } // Build index.js as a blocking IIFE bundle, matching the pre-Vite webpack behavior. @@ -74,14 +71,13 @@ function iifeIndexPlugin(): Plugin { async closeBundle() { // Clean up old hashed files before rebuilding for (const file of globSync('js/index.*.js*', {cwd: outDir})) unlinkSync(join(outDir, file)); - for (const file of globSync('js/webcomponents.*.js*', {cwd: outDir})) unlinkSync(join(outDir, file)); const result = await build(commonViteOpts({ build: { lib: { - entry: fileURLToPath(new URL('web_src/js/index.ts', import.meta.url)), + entry: join(import.meta.dirname, 'web_src/js/index.ts'), formats: ['iife'], - name: 'gitea', + name: 'iife', }, rolldownOptions: { output: { @@ -89,9 +85,6 @@ function iifeIndexPlugin(): Plugin { }, }, }, - define: { - 'process.env.NODE_ENV': JSON.stringify(isProduction ? 'production' : 'development'), - }, plugins: [ stringPlugin(), ], @@ -99,22 +92,13 @@ function iifeIndexPlugin(): Plugin { // Append IIFE index entry to the main Vite manifest const manifestPath = join(outDir, '.vite', 'manifest.json'); - let manifest: Manifest = {}; - try { manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) } catch {} - for (const buildOutput of (Array.isArray(result) ? result : [result])) { - if (!('output' in buildOutput)) continue; - const entry = buildOutput.output.find((o) => o.fileName.startsWith('js/index.')); - if (entry) { - manifest['web_src/js/index.ts'] = { - file: entry.fileName, - name: 'index', - isEntry: true, - }; - delete manifest['web_src/js/webcomponents/index.ts']; - writeFileSync(manifestPath, JSON.stringify(manifest, null, 2)); - break; - } - } + const buildOutput = (Array.isArray(result) ? result[0] : result) as Rolldown.RolldownOutput; + const entry = buildOutput.output.find((o) => o.fileName.startsWith('js/index.')); + if (!entry) throw new Error('IIFE index build produced no output'); + writeFileSync(manifestPath, JSON.stringify({ + ...JSON.parse(readFileSync(manifestPath, 'utf8')), + 'web_src/js/index.ts': {file: entry.fileName, name: 'index', isEntry: true}, + }, null, 2)); }, }; } @@ -139,12 +123,12 @@ export default defineConfig(commonViteOpts({ chunkSizeWarningLimit: Infinity, rolldownOptions: { input: { - 'index-domready': fileURLToPath(new URL('web_src/js/index-domready.ts', import.meta.url)), - swagger: fileURLToPath(new URL('web_src/js/standalone/swagger.ts', import.meta.url)), - 'external-render-iframe': fileURLToPath(new URL('web_src/js/standalone/external-render-iframe.ts', import.meta.url)), - sharedworker: fileURLToPath(new URL('web_src/js/features/sharedworker.ts', import.meta.url)), + 'index-domready': join(import.meta.dirname, 'web_src/js/index-domready.ts'), + swagger: join(import.meta.dirname, 'web_src/js/standalone/swagger.ts'), + 'external-render-iframe': join(import.meta.dirname, 'web_src/js/standalone/external-render-iframe.ts'), + sharedworker: join(import.meta.dirname, 'web_src/js/features/sharedworker.ts'), ...(!isProduction && { - devtest: fileURLToPath(new URL('web_src/js/standalone/devtest.ts', import.meta.url)), + devtest: join(import.meta.dirname, 'web_src/js/standalone/devtest.ts'), }), ...themes, }, @@ -159,12 +143,10 @@ export default defineConfig(commonViteOpts({ }, entryFileNames: 'js/[name].[hash:8].js', chunkFileNames: 'js/[name].[hash:8].js', - assetFileNames: (info: {name?: string}) => { - const name = (info.name ?? '').split('?')[0]; - if (/\.css$/i.test(name)) { - return 'css/[name].[hash:8].css'; - } - if (/\.(ttf|woff2?)$/i.test(name)) return 'fonts/[name].[hash:8].[ext]'; + assetFileNames: ({names}) => { + const name = names[0]; + if (name.endsWith('.css')) return 'css/[name].[hash:8].css'; + if (/\.(ttf|woff2?)$/.test(name)) return 'fonts/[name].[hash:8].[ext]'; return '[name].[hash:8].[ext]'; }, }, @@ -206,26 +188,25 @@ export default defineConfig(commonViteOpts({ vuePlugin({ template: { compilerOptions: { - isCustomElement: (tag: string) => webComponents.has(tag), + isCustomElement: (tag) => webComponents.has(tag), }, }, }), isProduction ? licensePlugin({ thirdParty: { output: { - file: fileURLToPath(new URL('public/assets/licenses.txt', import.meta.url)), - template(dependencies) { + file: join(import.meta.dirname, 'public/assets/licenses.txt'), + template(deps) { const line = '-'.repeat(80); - const goJson = readFileSync('assets/go-licenses.json', 'utf8'); - const goModules = JSON.parse(goJson).map(({name, licenseText}: Record) => { + const goJson = readFileSync(join(import.meta.dirname, 'assets/go-licenses.json'), 'utf8'); + const goModules = JSON.parse(goJson).map(({name, licenseText}: {name: string, licenseText: string}) => { return {name, body: formatLicenseText(licenseText)}; }); - const jsModules = dependencies.map((dep) => { + const jsModules = deps.map((dep) => { return {name: dep.name, version: dep.version, body: formatLicenseText(dep.licenseText ?? '')}; }); - const modules = [...goModules, ...jsModules].sort((a, b) => a.name.localeCompare(b.name)); - return modules.map(({name, version, body}: Record) => { + return modules.map(({name, version, body}: {name: string, version?: string, body: string}) => { const title = version ? `${name}@${version}` : name; return `${line}\n${title}\n${line}\n${body}`; }).join('\n'); From 80a31ec45f84d17fceea7b94486b6e7f8d442be1 Mon Sep 17 00:00:00 2001 From: silverwind Date: Sun, 15 Mar 2026 21:54:19 +0100 Subject: [PATCH 052/102] Add citation-js codeSplitting group, update comment Co-Authored-By: Claude (Opus 4.6) --- vite.config.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/vite.config.ts b/vite.config.ts index e05e1259f0689..9038a8a2828e9 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -134,10 +134,12 @@ export default defineConfig(commonViteOpts({ }, output: { codeSplitting: { - // avoid absorbing shared deps into groups, which can make lazy chunks static + // prevent mermaid group from absorbing shared deps (like preload-helper), + // which would make the lazy mermaid chunk a static dependency of the entry includeDependenciesRecursively: false, groups: [ - {name: 'mermaid', test: /[\\/]mermaid[\\/]|@mermaid-js[\\/]/, priority: 10}, + {name: 'mermaid', test: /[\\/]mermaid[\\/]|@mermaid-js[\\/]/}, + {name: 'citation-js', test: /@citation-js[\\/]/}, {name: 'vue', test: /[\\/]@vue[\\/]/}, ], }, From 2101c2e4254376c1d4a44f7890bdff1bd3023a66 Mon Sep 17 00:00:00 2001 From: silverwind Date: Sun, 15 Mar 2026 22:06:46 +0100 Subject: [PATCH 053/102] explicit minify --- vite.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vite.config.ts b/vite.config.ts index 9038a8a2828e9..a918dc78be69e 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -51,7 +51,7 @@ function commonViteOpts({build, ...other}: InlineConfig): InlineConfig { emptyOutDir: false, sourcemap: enableSourcemap, target: 'es2020', - minify: isProduction, + minify: isProduction ? 'oxc' : false, cssMinify: isProduction ? 'esbuild' : false, reportCompressedSize: false, rolldownOptions: { From 06482175074a31fee44185138d3a928e437d7f28 Mon Sep 17 00:00:00 2001 From: silverwind Date: Sun, 15 Mar 2026 22:19:02 +0100 Subject: [PATCH 054/102] Restore process.env.NODE_ENV define for IIFE build, remove citation-js group Tippy.js checks process.env.NODE_ENV and is pulled into the IIFE bundle via overflow-menu.ts -> modules/tippy.ts. The citation-js codeSplitting group breaks plugin registration because includeDependenciesRecursively: false leaves the Cite class in a separate chunk. Co-Authored-By: Claude (Opus 4.6) --- vite.config.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/vite.config.ts b/vite.config.ts index a918dc78be69e..b41b532fb2aa4 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -85,6 +85,10 @@ function iifeIndexPlugin(): Plugin { }, }, }, + define: { + // needed for tippy.js pulled into this chunk via overflow-menu.ts + 'process.env.NODE_ENV': JSON.stringify(isProduction ? 'production' : 'development'), + }, plugins: [ stringPlugin(), ], @@ -139,7 +143,6 @@ export default defineConfig(commonViteOpts({ includeDependenciesRecursively: false, groups: [ {name: 'mermaid', test: /[\\/]mermaid[\\/]|@mermaid-js[\\/]/}, - {name: 'citation-js', test: /@citation-js[\\/]/}, {name: 'vue', test: /[\\/]@vue[\\/]/}, ], }, From 4797006733c7d6c4c94d165aa3efa5f59ba6d7c7 Mon Sep 17 00:00:00 2001 From: silverwind Date: Tue, 17 Mar 2026 19:32:52 +0100 Subject: [PATCH 055/102] Remove stale fomantic.css import The fomantic CSS was migrated to first-party modules in #36869, but the import in index-domready.ts was left behind after merge. Co-Authored-By: Claude (Opus 4.6) --- web_src/js/index-domready.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/web_src/js/index-domready.ts b/web_src/js/index-domready.ts index bf5915ecb6913..cc1edadb90664 100644 --- a/web_src/js/index-domready.ts +++ b/web_src/js/index-domready.ts @@ -1,5 +1,4 @@ import '../fomantic/build/fomantic.js'; -import '../fomantic/build/fomantic.css'; import '../css/index.css'; import type {HtmxResponseInfo} from 'htmx.org'; import {showErrorToast} from './modules/toast.ts'; From d890d9a86b350bf3a19b017e234dbfb4e832f6f8 Mon Sep 17 00:00:00 2001 From: silverwind Date: Mon, 23 Mar 2026 18:33:05 +0100 Subject: [PATCH 056/102] Apply suggestion from @silverwind Signed-off-by: silverwind --- vite.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vite.config.ts b/vite.config.ts index b41b532fb2aa4..2fd68a97aae2d 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -64,7 +64,7 @@ function commonViteOpts({build, ...other}: InlineConfig): InlineConfig { }; } -// Build index.js as a blocking IIFE bundle, matching the pre-Vite webpack behavior. +// Build index.js as a blocking IIFE bundle to avoid pop-in effects. function iifeIndexPlugin(): Plugin { return { name: 'iife-index', From 16eaa6c94249c0320c15577e5a091fa83c5b0361 Mon Sep 17 00:00:00 2001 From: silverwind Date: Mon, 23 Mar 2026 18:35:02 +0100 Subject: [PATCH 057/102] Apply suggestion from @silverwind Signed-off-by: silverwind --- web_src/js/vitest.setup.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/web_src/js/vitest.setup.ts b/web_src/js/vitest.setup.ts index e155d7cbe4d7b..42a6bd3335bbf 100644 --- a/web_src/js/vitest.setup.ts +++ b/web_src/js/vitest.setup.ts @@ -1,5 +1,6 @@ // Stub APIs not implemented by happy-dom but needed by dependencies // XPathEvaluator is used by htmx at module evaluation time +// TODO: Remove after https://github.com/capricorn86/happy-dom/pull/2103 is released if (!globalThis.XPathEvaluator) { globalThis.XPathEvaluator = class { createExpression() { return {evaluate: () => ({iterateNext: () => null})} } From e9df9c2f94bc7ae2dacb98bc70adb6d704fcc52c Mon Sep 17 00:00:00 2001 From: silverwind Date: Mon, 23 Mar 2026 19:11:05 +0100 Subject: [PATCH 058/102] disable custom codeSplitting --- package.json | 4 +- pnpm-lock.yaml | 197 ++++++++++++++++++++++++------------------------- vite.config.ts | 13 +--- 3 files changed, 99 insertions(+), 115 deletions(-) diff --git a/package.json b/package.json index c399b17244d6b..46b23b5ff6ac3 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "tributejs": "5.1.3", "uint8-to-base64": "0.2.1", "vanilla-colorful": "0.7.2", - "vite": "8.0.0", + "vite": "8.0.2", "vite-string-plugin": "2.0.2", "vue": "3.5.29", "vue-bar-graph": "2.2.0", @@ -78,7 +78,7 @@ "@types/throttle-debounce": "5.0.2", "@types/toastify-js": "1.12.4", "@typescript-eslint/parser": "8.57.1", - "@vitejs/plugin-vue": "6.0.4", + "@vitejs/plugin-vue": "6.0.5", "@vitest/eslint-plugin": "1.6.12", "eslint": "10.0.3", "eslint-import-resolver-typescript": "4.4.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5867b40b659af..26d89179ed7d0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -64,7 +64,7 @@ importers: version: 2.1.1(tippy.js@6.3.7)(vue@3.5.29(typescript@5.9.3)) '@vitejs/plugin-vue': specifier: 6.0.5 - version: 6.0.5(vite@8.0.0(@types/node@25.3.5)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2))(vue@3.5.29(typescript@5.9.3)) + version: 6.0.5(vite@8.0.2(@types/node@25.3.5)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2))(vue@3.5.29(typescript@5.9.3)) ansi_up: specifier: 6.0.6 version: 6.0.6 @@ -168,11 +168,11 @@ importers: specifier: 0.7.2 version: 0.7.2 vite: - specifier: 8.0.0 - version: 8.0.0(@types/node@25.3.5)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2) + specifier: 8.0.2 + version: 8.0.2(@types/node@25.3.5)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2) vite-string-plugin: specifier: 2.0.2 - version: 2.0.2(vite@8.0.0(@types/node@25.3.5)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2)) + version: 2.0.2(vite@8.0.2(@types/node@25.3.5)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2)) vue: specifier: 3.5.29 version: 3.5.29(typescript@5.9.3) @@ -239,7 +239,7 @@ importers: version: 8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) '@vitest/eslint-plugin': specifier: 1.6.12 - version: 1.6.12(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3)(vitest@4.1.0(@types/node@25.3.5)(happy-dom@20.8.3)(vite@8.0.0(@types/node@25.3.5)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2))) + version: 1.6.12(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3)(vitest@4.1.0(@types/node@25.3.5)(happy-dom@20.8.3)(vite@8.0.2(@types/node@25.3.5)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2))) eslint: specifier: 10.0.3 version: 10.0.3(jiti@2.6.1) @@ -332,7 +332,7 @@ importers: version: 17.8.3 vitest: specifier: 4.1.0 - version: 4.1.0(@types/node@25.3.5)(happy-dom@20.8.3)(vite@8.0.0(@types/node@25.3.5)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2)) + version: 4.1.0(@types/node@25.3.5)(happy-dom@20.8.3)(vite@8.0.2(@types/node@25.3.5)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2)) vue-tsc: specifier: 3.2.5 version: 3.2.5(typescript@5.9.3) @@ -868,12 +868,8 @@ packages: resolution: {integrity: sha512-3dsKlf4Ma7o+uxLIg5OI1Tgwfet2pE8WTbPjEGWvOe6CSjMtK0skJnnSVHaEVX4N4mYU81To0qDeZOPqjaUotg==} engines: {node: '>=12.4.0'} - '@oxc-project/runtime@0.115.0': - resolution: {integrity: sha512-Rg8Wlt5dCbXhQnsXPrkOjL1DTSvXLgb2R/KYfnf1/K+R0k6UMLEmbQXPM+kwrWqSmWA2t0B1EtHy2/3zikQpvQ==} - engines: {node: ^20.19.0 || >=22.12.0} - - '@oxc-project/types@0.115.0': - resolution: {integrity: sha512-4n91DKnebUS4yjUHl2g3/b2T+IUdCfmoZGhmwsovZCDaJSs+QkVAM+0AqqTxHSsHfeiMuueT75cZaZcT/m0pSw==} + '@oxc-project/types@0.122.0': + resolution: {integrity: sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==} '@package-json/types@0.0.12': resolution: {integrity: sha512-uu43FGU34B5VM9mCNjXCwLaGHYjXdNincqKLaraaCW+7S2+SmiBg1Nv8bPnmschrIfZmfKNY9f3fC376MRrObw==} @@ -897,107 +893,107 @@ packages: resolution: {integrity: sha512-FqALmHI8D4o6lk/LRWDnhw95z5eO+eAa6ORjVg09YRR7BkcM6oPHU9uyC0gtQG5vpFLvgpeU4+zEAz2H8APHNw==} engines: {node: '>= 10'} - '@rolldown/binding-android-arm64@1.0.0-rc.9': - resolution: {integrity: sha512-lcJL0bN5hpgJfSIz/8PIf02irmyL43P+j1pTCfbD1DbLkmGRuFIA4DD3B3ZOvGqG0XiVvRznbKtN0COQVaKUTg==} + '@rolldown/binding-android-arm64@1.0.0-rc.11': + resolution: {integrity: sha512-SJ+/g+xNnOh6NqYxD0V3uVN4W3VfnrGsC9/hoglicgTNfABFG9JjISvkkU0dNY84MNHLWyOgxP9v9Y9pX4S7+A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rolldown/binding-darwin-arm64@1.0.0-rc.9': - resolution: {integrity: sha512-J7Zk3kLYFsLtuH6U+F4pS2sYVzac0qkjcO5QxHS7OS7yZu2LRs+IXo+uvJ/mvpyUljDJ3LROZPoQfgBIpCMhdQ==} + '@rolldown/binding-darwin-arm64@1.0.0-rc.11': + resolution: {integrity: sha512-7WQgR8SfOPwmDZGFkThUvsmd/nwAWv91oCO4I5LS7RKrssPZmOt7jONN0cW17ydGC1n/+puol1IpoieKqQidmg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.0.0-rc.9': - resolution: {integrity: sha512-iwtmmghy8nhfRGeNAIltcNXzD0QMNaaA5U/NyZc1Ia4bxrzFByNMDoppoC+hl7cDiUq5/1CnFthpT9n+UtfFyg==} + '@rolldown/binding-darwin-x64@1.0.0-rc.11': + resolution: {integrity: sha512-39Ks6UvIHq4rEogIfQBoBRusj0Q0nPVWIvqmwBLaT6aqQGIakHdESBVOPRRLacy4WwUPIx4ZKzfZ9PMW+IeyUQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.0.0-rc.9': - resolution: {integrity: sha512-DLFYI78SCiZr5VvdEplsVC2Vx53lnA4/Ga5C65iyldMVaErr86aiqCoNBLl92PXPfDtUYjUh+xFFor40ueNs4Q==} + '@rolldown/binding-freebsd-x64@1.0.0-rc.11': + resolution: {integrity: sha512-jfsm0ZHfhiqrvWjJAmzsqiIFPz5e7mAoCOPBNTcNgkiid/LaFKiq92+0ojH+nmJmKYkre4t71BWXUZDNp7vsag==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.9': - resolution: {integrity: sha512-CsjTmTwd0Hri6iTw/DRMK7kOZ7FwAkrO4h8YWKoX/kcj833e4coqo2wzIFywtch/8Eb5enQ/lwLM7w6JX1W5RQ==} + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.11': + resolution: {integrity: sha512-zjQaUtSyq1nVe3nxmlSCuR96T1LPlpvmJ0SZy0WJFEsV4kFbXcq2u68L4E6O0XeFj4aex9bEauqjW8UQBeAvfQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.9': - resolution: {integrity: sha512-2x9O2JbSPxpxMDhP9Z74mahAStibTlrBMW0520+epJH5sac7/LwZW5Bmg/E6CXuEF53JJFW509uP+lSedaUNxg==} + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.11': + resolution: {integrity: sha512-WMW1yE6IOnehTcFE9eipFkm3XN63zypWlrJQ2iF7NrQ9b2LDRjumFoOGJE8RJJTJCTBAdmLMnJ8uVitACUUo1Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-arm64-musl@1.0.0-rc.9': - resolution: {integrity: sha512-JA1QRW31ogheAIRhIg9tjMfsYbglXXYGNPLdPEYrwFxdbkQCAzvpSCSHCDWNl4hTtrol8WeboCSEpjdZK8qrCg==} + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.11': + resolution: {integrity: sha512-jfndI9tsfm4APzjNt6QdBkYwre5lRPUgHeDHoI7ydKUuJvz3lZeCfMsI56BZj+7BYqiKsJm7cfd/6KYV7ubrBg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.9': - resolution: {integrity: sha512-aOKU9dJheda8Kj8Y3w9gnt9QFOO+qKPAl8SWd7JPHP+Cu0EuDAE5wokQubLzIDQWg2myXq2XhTpOVS07qqvT+w==} + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.11': + resolution: {integrity: sha512-ZlFgw46NOAGMgcdvdYwAGu2Q+SLFA9LzbJLW+iyMOJyhj5wk6P3KEE9Gct4xWwSzFoPI7JCdYmYMzVtlgQ+zfw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.9': - resolution: {integrity: sha512-OalO94fqj7IWRn3VdXWty75jC5dk4C197AWEuMhIpvVv2lw9fiPhud0+bW2ctCxb3YoBZor71QHbY+9/WToadA==} + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.11': + resolution: {integrity: sha512-hIOYmuT6ofM4K04XAZd3OzMySEO4K0/nc9+jmNcxNAxRi6c5UWpqfw3KMFV4MVFWL+jQsSh+bGw2VqmaPMTLyw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.0.0-rc.9': - resolution: {integrity: sha512-cVEl1vZtBsBZna3YMjGXNvnYYrOJ7RzuWvZU0ffvJUexWkukMaDuGhUXn0rjnV0ptzGVkvc+vW9Yqy6h8YX4pg==} + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.11': + resolution: {integrity: sha512-qXBQQO9OvkjjQPLdUVr7Nr2t3QTZI7s4KZtfw7HzBgjbmAPSFwSv4rmET9lLSgq3rH/ndA3ngv3Qb8l2njoPNA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-musl@1.0.0-rc.9': - resolution: {integrity: sha512-UzYnKCIIc4heAKgI4PZ3dfBGUZefGCJ1TPDuLHoCzgrMYPb5Rv6TLFuYtyM4rWyHM7hymNdsg5ik2C+UD9VDbA==} + '@rolldown/binding-linux-x64-musl@1.0.0-rc.11': + resolution: {integrity: sha512-/tpFfoSTzUkH9LPY+cYbqZBDyyX62w5fICq9qzsHLL8uTI6BHip3Q9Uzft0wylk/i8OOwKik8OxW+QAhDmzwmg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@rolldown/binding-openharmony-arm64@1.0.0-rc.9': - resolution: {integrity: sha512-+6zoiF+RRyf5cdlFQP7nm58mq7+/2PFaY2DNQeD4B87N36JzfF/l9mdBkkmTvSYcYPE8tMh/o3cRlsx1ldLfog==} + '@rolldown/binding-openharmony-arm64@1.0.0-rc.11': + resolution: {integrity: sha512-mcp3Rio2w72IvdZG0oQ4bM2c2oumtwHfUfKncUM6zGgz0KgPz4YmDPQfnXEiY5t3+KD/i8HG2rOB/LxdmieK2g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rolldown/binding-wasm32-wasi@1.0.0-rc.9': - resolution: {integrity: sha512-rgFN6sA/dyebil3YTlL2evvi/M+ivhfnyxec7AccTpRPccno/rPoNlqybEZQBkcbZu8Hy+eqNJCqfBR8P7Pg8g==} + '@rolldown/binding-wasm32-wasi@1.0.0-rc.11': + resolution: {integrity: sha512-LXk5Hii1Ph9asuGRjBuz8TUxdc1lWzB7nyfdoRgI0WGPZKmCxvlKk8KfYysqtr4MfGElu/f/pEQRh8fcEgkrWw==} engines: {node: '>=14.0.0'} cpu: [wasm32] - '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.9': - resolution: {integrity: sha512-lHVNUG/8nlF1IQk1C0Ci574qKYyty2goMiPlRqkC5R+3LkXDkL5Dhx8ytbxq35m+pkHVIvIxviD+TWLdfeuadA==} + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.11': + resolution: {integrity: sha512-dDwf5otnx0XgRY1yqxOC4ITizcdzS/8cQ3goOWv3jFAo4F+xQYni+hnMuO6+LssHHdJW7+OCVL3CoU4ycnh35Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.0.0-rc.9': - resolution: {integrity: sha512-G0oA4+w1iY5AGi5HcDTxWsoxF509hrFIPB2rduV5aDqS9FtDg1CAfa7V34qImbjfhIcA8C+RekocJZA96EarwQ==} + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.11': + resolution: {integrity: sha512-LN4/skhSggybX71ews7dAj6r2geaMJfm3kMbK2KhFMg9B10AZXnKoLCVVgzhMHL0S+aKtr4p8QbAW8k+w95bAA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] + '@rolldown/pluginutils@1.0.0-rc.11': + resolution: {integrity: sha512-xQO9vbwBecJRv9EUcQ/y0dzSTJgA7Q6UVN7xp6B81+tBGSLVAK03yJ9NkJaUA7JFD91kbjxRSC/mDnmvXzbHoQ==} + '@rolldown/pluginutils@1.0.0-rc.2': resolution: {integrity: sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw==} - '@rolldown/pluginutils@1.0.0-rc.9': - resolution: {integrity: sha512-w6oiRWgEBl04QkFZgmW+jnU1EC9b57Oihi2ot3HNWIQRqgHp5PnYDia5iZ5FF7rpa4EQdiqMDXjlqKGXBhsoXw==} - '@rollup/rollup-android-arm-eabi@4.59.0': resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==} cpu: [arm] @@ -3518,8 +3514,8 @@ packages: robust-predicates@3.0.2: resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==} - rolldown@1.0.0-rc.9: - resolution: {integrity: sha512-9EbgWge7ZH+yqb4d2EnELAntgPTWbfL8ajiTW+SyhJEC4qhBbkCKbqFV4Ge4zmu5ziQuVbWxb/XwLZ+RIO7E8Q==} + rolldown@1.0.0-rc.11: + resolution: {integrity: sha512-NRjoKMusSjfRbSYiH3VSumlkgFe7kYAa3pzVOsVYVFY3zb5d7nS+a3KGQ7hJKXuYWbzJKPVQ9Wxq2UvyK+ENpw==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -3914,13 +3910,13 @@ packages: peerDependencies: vite: '*' - vite@8.0.0: - resolution: {integrity: sha512-fPGaRNj9Zytaf8LEiBhY7Z6ijnFKdzU/+mL8EFBaKr7Vw1/FWcTBAMW0wLPJAGMPX38ZPVCVgLceWiEqeoqL2Q==} + vite@8.0.2: + resolution: {integrity: sha512-1gFhNi+bHhRE/qKZOJXACm6tX4bA3Isy9KuKF15AgSRuRazNBOJfdDemPBU16/mpMxApDPrWvZ08DcLPEoRnuA==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: '@types/node': ^20.19.0 || >=22.12.0 - '@vitejs/devtools': ^0.0.0-alpha.31 + '@vitejs/devtools': ^0.1.0 esbuild: ^0.27.0 jiti: '>=1.21.0' less: ^4.0.0 @@ -4572,9 +4568,7 @@ snapshots: dependencies: '@nolyfill/shared': 1.0.44 - '@oxc-project/runtime@0.115.0': {} - - '@oxc-project/types@0.115.0': {} + '@oxc-project/types@0.122.0': {} '@package-json/types@0.0.12': {} @@ -4592,56 +4586,56 @@ snapshots: '@resvg/resvg-wasm@2.6.2': {} - '@rolldown/binding-android-arm64@1.0.0-rc.9': + '@rolldown/binding-android-arm64@1.0.0-rc.11': optional: true - '@rolldown/binding-darwin-arm64@1.0.0-rc.9': + '@rolldown/binding-darwin-arm64@1.0.0-rc.11': optional: true - '@rolldown/binding-darwin-x64@1.0.0-rc.9': + '@rolldown/binding-darwin-x64@1.0.0-rc.11': optional: true - '@rolldown/binding-freebsd-x64@1.0.0-rc.9': + '@rolldown/binding-freebsd-x64@1.0.0-rc.11': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.9': + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.11': optional: true - '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.9': + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.11': optional: true - '@rolldown/binding-linux-arm64-musl@1.0.0-rc.9': + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.11': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.9': + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.11': optional: true - '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.9': + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.11': optional: true - '@rolldown/binding-linux-x64-gnu@1.0.0-rc.9': + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.11': optional: true - '@rolldown/binding-linux-x64-musl@1.0.0-rc.9': + '@rolldown/binding-linux-x64-musl@1.0.0-rc.11': optional: true - '@rolldown/binding-openharmony-arm64@1.0.0-rc.9': + '@rolldown/binding-openharmony-arm64@1.0.0-rc.11': optional: true - '@rolldown/binding-wasm32-wasi@1.0.0-rc.9': + '@rolldown/binding-wasm32-wasi@1.0.0-rc.11': dependencies: '@napi-rs/wasm-runtime': 1.1.1 optional: true - '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.9': + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.11': optional: true - '@rolldown/binding-win32-x64-msvc@1.0.0-rc.9': + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.11': optional: true - '@rolldown/pluginutils@1.0.0-rc.2': {} + '@rolldown/pluginutils@1.0.0-rc.11': {} - '@rolldown/pluginutils@1.0.0-rc.9': {} + '@rolldown/pluginutils@1.0.0-rc.2': {} '@rollup/rollup-android-arm-eabi@4.59.0': optional: true @@ -5199,20 +5193,20 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.11.1': optional: true - '@vitejs/plugin-vue@6.0.5(vite@8.0.0(@types/node@25.3.5)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2))(vue@3.5.29(typescript@5.9.3))': + '@vitejs/plugin-vue@6.0.5(vite@8.0.2(@types/node@25.3.5)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2))(vue@3.5.29(typescript@5.9.3))': dependencies: '@rolldown/pluginutils': 1.0.0-rc.2 - vite: 8.0.0(@types/node@25.3.5)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2) + vite: 8.0.2(@types/node@25.3.5)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2) vue: 3.5.29(typescript@5.9.3) - '@vitest/eslint-plugin@1.6.12(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3)(vitest@4.1.0(@types/node@25.3.5)(happy-dom@20.8.3)(vite@8.0.0(@types/node@25.3.5)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2)))': + '@vitest/eslint-plugin@1.6.12(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3)(vitest@4.1.0(@types/node@25.3.5)(happy-dom@20.8.3)(vite@8.0.2(@types/node@25.3.5)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2)))': dependencies: '@typescript-eslint/scope-manager': 8.56.1 '@typescript-eslint/utils': 8.56.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) eslint: 10.0.3(jiti@2.6.1) optionalDependencies: typescript: 5.9.3 - vitest: 4.1.0(@types/node@25.3.5)(happy-dom@20.8.3)(vite@8.0.0(@types/node@25.3.5)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2)) + vitest: 4.1.0(@types/node@25.3.5)(happy-dom@20.8.3)(vite@8.0.2(@types/node@25.3.5)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2)) transitivePeerDependencies: - supports-color @@ -5225,13 +5219,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.0.3 - '@vitest/mocker@4.1.0(vite@8.0.0(@types/node@25.3.5)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2))': + '@vitest/mocker@4.1.0(vite@8.0.2(@types/node@25.3.5)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2))': dependencies: '@vitest/spy': 4.1.0 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.0.0(@types/node@25.3.5)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2) + vite: 8.0.2(@types/node@25.3.5)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2) '@vitest/pretty-format@4.1.0': dependencies: @@ -7256,26 +7250,26 @@ snapshots: robust-predicates@3.0.2: {} - rolldown@1.0.0-rc.9: + rolldown@1.0.0-rc.11: dependencies: - '@oxc-project/types': 0.115.0 - '@rolldown/pluginutils': 1.0.0-rc.9 + '@oxc-project/types': 0.122.0 + '@rolldown/pluginutils': 1.0.0-rc.11 optionalDependencies: - '@rolldown/binding-android-arm64': 1.0.0-rc.9 - '@rolldown/binding-darwin-arm64': 1.0.0-rc.9 - '@rolldown/binding-darwin-x64': 1.0.0-rc.9 - '@rolldown/binding-freebsd-x64': 1.0.0-rc.9 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.9 - '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.9 - '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.9 - '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.9 - '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.9 - '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.9 - '@rolldown/binding-linux-x64-musl': 1.0.0-rc.9 - '@rolldown/binding-openharmony-arm64': 1.0.0-rc.9 - '@rolldown/binding-wasm32-wasi': 1.0.0-rc.9 - '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.9 - '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.9 + '@rolldown/binding-android-arm64': 1.0.0-rc.11 + '@rolldown/binding-darwin-arm64': 1.0.0-rc.11 + '@rolldown/binding-darwin-x64': 1.0.0-rc.11 + '@rolldown/binding-freebsd-x64': 1.0.0-rc.11 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.11 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.11 + '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.11 + '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.11 + '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.11 + '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.11 + '@rolldown/binding-linux-x64-musl': 1.0.0-rc.11 + '@rolldown/binding-openharmony-arm64': 1.0.0-rc.11 + '@rolldown/binding-wasm32-wasi': 1.0.0-rc.11 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.11 + '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.11 rollup-plugin-license@3.7.0(picomatch@4.0.3)(rollup@4.59.0): dependencies: @@ -7759,17 +7753,16 @@ snapshots: vanilla-colorful@0.7.2: {} - vite-string-plugin@2.0.2(vite@8.0.0(@types/node@25.3.5)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2)): + vite-string-plugin@2.0.2(vite@8.0.2(@types/node@25.3.5)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2)): dependencies: - vite: 8.0.0(@types/node@25.3.5)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2) + vite: 8.0.2(@types/node@25.3.5)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2) - vite@8.0.0(@types/node@25.3.5)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2): + vite@8.0.2(@types/node@25.3.5)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2): dependencies: - '@oxc-project/runtime': 0.115.0 lightningcss: 1.32.0 picomatch: 4.0.3 postcss: 8.5.8 - rolldown: 1.0.0-rc.9 + rolldown: 1.0.0-rc.11 tinyglobby: 0.2.15 optionalDependencies: '@types/node': 25.3.5 @@ -7779,10 +7772,10 @@ snapshots: terser: 5.46.0 yaml: 2.8.2 - vitest@4.1.0(@types/node@25.3.5)(happy-dom@20.8.3)(vite@8.0.0(@types/node@25.3.5)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2)): + vitest@4.1.0(@types/node@25.3.5)(happy-dom@20.8.3)(vite@8.0.2(@types/node@25.3.5)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2)): dependencies: '@vitest/expect': 4.1.0 - '@vitest/mocker': 4.1.0(vite@8.0.0(@types/node@25.3.5)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2)) + '@vitest/mocker': 4.1.0(vite@8.0.2(@types/node@25.3.5)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2)) '@vitest/pretty-format': 4.1.0 '@vitest/runner': 4.1.0 '@vitest/snapshot': 4.1.0 @@ -7799,7 +7792,7 @@ snapshots: tinyexec: 1.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 - vite: 8.0.0(@types/node@25.3.5)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2) + vite: 8.0.2(@types/node@25.3.5)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.0)(yaml@2.8.2) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 25.3.5 diff --git a/vite.config.ts b/vite.config.ts index 2fd68a97aae2d..6bb58398a11af 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -64,7 +64,7 @@ function commonViteOpts({build, ...other}: InlineConfig): InlineConfig { }; } -// Build index.js as a blocking IIFE bundle to avoid pop-in effects. +// Build index.js as a blocking IIFE bundle to avoid pop-in effects function iifeIndexPlugin(): Plugin { return { name: 'iife-index', @@ -86,7 +86,7 @@ function iifeIndexPlugin(): Plugin { }, }, define: { - // needed for tippy.js pulled into this chunk via overflow-menu.ts + // needed for tippy.js 'process.env.NODE_ENV': JSON.stringify(isProduction ? 'production' : 'development'), }, plugins: [ @@ -137,15 +137,6 @@ export default defineConfig(commonViteOpts({ ...themes, }, output: { - codeSplitting: { - // prevent mermaid group from absorbing shared deps (like preload-helper), - // which would make the lazy mermaid chunk a static dependency of the entry - includeDependenciesRecursively: false, - groups: [ - {name: 'mermaid', test: /[\\/]mermaid[\\/]|@mermaid-js[\\/]/}, - {name: 'vue', test: /[\\/]@vue[\\/]/}, - ], - }, entryFileNames: 'js/[name].[hash:8].js', chunkFileNames: 'js/[name].[hash:8].js', assetFileNames: ({names}) => { From 5e037c88927757eb7e3dfef1343892e9983380d7 Mon Sep 17 00:00:00 2001 From: silverwind Date: Mon, 23 Mar 2026 19:26:27 +0100 Subject: [PATCH 059/102] Increase default `STATIC_CACHE_TIME` from 6h to 30 days All hashed static assets are effectively immutable, so a longer cache duration is safe and reduces unnecessary revalidation requests. Co-Authored-By: Claude (Opus 4.6) --- custom/conf/app.example.ini | 4 ++-- modules/setting/server.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 9297f3d062721..4e1c0b22f2262 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -342,8 +342,8 @@ RUN_USER = ; git ;; 0 disables this. ;STARTUP_TIMEOUT = 0 ;; -;; Static resources, includes resources on custom/, public/ and all uploaded avatars web browser cache time. Note that this cache is disabled when RUN_MODE is "dev". Default is 6h -;STATIC_CACHE_TIME = 6h +;; Static resources, includes resources on custom/, public/ and all uploaded avatars web browser cache time. Note that this cache is disabled when RUN_MODE is "dev". Default is 720h (30 days) +;STATIC_CACHE_TIME = 720h ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/modules/setting/server.go b/modules/setting/server.go index 36342dfdbe9e8..4a90d8bb3fb0f 100644 --- a/modules/setting/server.go +++ b/modules/setting/server.go @@ -345,7 +345,7 @@ func loadServerFrom(rootCfg ConfigProvider) { StaticRootPath = AppWorkPath } StaticRootPath = sec.Key("STATIC_ROOT_PATH").MustString(StaticRootPath) - StaticCacheTime = sec.Key("STATIC_CACHE_TIME").MustDuration(6 * time.Hour) + StaticCacheTime = sec.Key("STATIC_CACHE_TIME").MustDuration(720 * time.Hour) AppDataPath = sec.Key("APP_DATA_PATH").MustString(filepath.Join(AppWorkPath, "data")) if !filepath.IsAbs(AppDataPath) { AppDataPath = filepath.ToSlash(filepath.Join(AppWorkPath, AppDataPath)) From f0d12baf5ae7a919cc6c80a910643f28266778a2 Mon Sep 17 00:00:00 2001 From: silverwind Date: Tue, 24 Mar 2026 01:00:17 +0100 Subject: [PATCH 060/102] Rename index-domready to index, index to iife Rename the IIFE blocking bundle from index.ts to iife.ts and rename the module entry from index-domready.ts to index.ts. This gives the main CSS output a clean name (css/index.css) instead of the previous css/index-domready.css. Co-Authored-By: Claude (Opus 4.6) --- modules/public/manifest.go | 2 +- templates/base/footer.tmpl | 2 +- templates/base/head_script.tmpl | 2 +- templates/base/head_style.tmpl | 2 +- vite.config.ts | 24 ++-- web_src/js/iife.ts | 11 ++ web_src/js/index-domready.ts | 188 ------------------------------- web_src/js/index.ts | 193 ++++++++++++++++++++++++++++++-- 8 files changed, 212 insertions(+), 212 deletions(-) create mode 100644 web_src/js/iife.ts delete mode 100644 web_src/js/index-domready.ts diff --git a/modules/public/manifest.go b/modules/public/manifest.go index 169a25915b57d..2b8c0681d1efb 100644 --- a/modules/public/manifest.go +++ b/modules/public/manifest.go @@ -52,7 +52,7 @@ func parseManifest(data []byte) map[string]string { ext := path.Ext(entry.File) key := dir + "/" + entry.Name + ext paths[key] = entry.File - // Map associated CSS files, e.g. "css/index-domready.css" -> "css/index-domready.B3zrQPqD.css" + // 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 diff --git a/templates/base/footer.tmpl b/templates/base/footer.tmpl index b312d243a15a0..b775212172dd8 100644 --- a/templates/base/footer.tmpl +++ b/templates/base/footer.tmpl @@ -9,7 +9,7 @@ {{template "custom/body_outer_post" .}} {{template "base/footer_content" .}} - + {{template "custom/footer" .}} diff --git a/templates/base/head_script.tmpl b/templates/base/head_script.tmpl index 031685eb3c728..7585c278a5729 100644 --- a/templates/base/head_script.tmpl +++ b/templates/base/head_script.tmpl @@ -31,4 +31,4 @@ If you introduce mistakes in it, Gitea JavaScript code wouldn't run correctly. {{/* in case some pages don't render the pageData, we make sure it is an object to prevent null access */}} window.config.pageData = window.config.pageData || {}; - + diff --git a/templates/base/head_style.tmpl b/templates/base/head_style.tmpl index 7446d4f639e1f..75aaf77dda0b7 100644 --- a/templates/base/head_style.tmpl +++ b/templates/base/head_style.tmpl @@ -1,2 +1,2 @@ - + diff --git a/vite.config.ts b/vite.config.ts index 6bb58398a11af..f8661544f1b25 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -64,24 +64,24 @@ function commonViteOpts({build, ...other}: InlineConfig): InlineConfig { }; } -// Build index.js as a blocking IIFE bundle to avoid pop-in effects -function iifeIndexPlugin(): Plugin { +// Build iife.js as a blocking IIFE bundle to avoid pop-in effects +function iifePlugin(): Plugin { return { - name: 'iife-index', + name: 'iife', async closeBundle() { // Clean up old hashed files before rebuilding - for (const file of globSync('js/index.*.js*', {cwd: outDir})) unlinkSync(join(outDir, file)); + for (const file of globSync('js/iife.*.js*', {cwd: outDir})) unlinkSync(join(outDir, file)); const result = await build(commonViteOpts({ build: { lib: { - entry: join(import.meta.dirname, 'web_src/js/index.ts'), + entry: join(import.meta.dirname, 'web_src/js/iife.ts'), formats: ['iife'], name: 'iife', }, rolldownOptions: { output: { - entryFileNames: 'js/index.[hash:8].js', + entryFileNames: 'js/iife.[hash:8].js', }, }, }, @@ -94,14 +94,14 @@ function iifeIndexPlugin(): Plugin { ], })); - // Append IIFE index entry to the main Vite manifest + // Append IIFE entry to the main Vite manifest const manifestPath = join(outDir, '.vite', 'manifest.json'); const buildOutput = (Array.isArray(result) ? result[0] : result) as Rolldown.RolldownOutput; - const entry = buildOutput.output.find((o) => o.fileName.startsWith('js/index.')); - if (!entry) throw new Error('IIFE index build produced no output'); + const entry = buildOutput.output.find((o) => o.fileName.startsWith('js/iife.')); + if (!entry) throw new Error('IIFE build produced no output'); writeFileSync(manifestPath, JSON.stringify({ ...JSON.parse(readFileSync(manifestPath, 'utf8')), - 'web_src/js/index.ts': {file: entry.fileName, name: 'index', isEntry: true}, + 'web_src/js/iife.ts': {file: entry.fileName, name: 'iife', isEntry: true}, }, null, 2)); }, }; @@ -127,7 +127,7 @@ export default defineConfig(commonViteOpts({ chunkSizeWarningLimit: Infinity, rolldownOptions: { input: { - 'index-domready': join(import.meta.dirname, 'web_src/js/index-domready.ts'), + index: join(import.meta.dirname, 'web_src/js/index.ts'), swagger: join(import.meta.dirname, 'web_src/js/standalone/swagger.ts'), 'external-render-iframe': join(import.meta.dirname, 'web_src/js/standalone/external-render-iframe.ts'), sharedworker: join(import.meta.dirname, 'web_src/js/features/sharedworker.ts'), @@ -178,7 +178,7 @@ export default defineConfig(commonViteOpts({ __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: false, }, plugins: [ - iifeIndexPlugin(), + iifePlugin(), filterCssUrlPlugin(), stringPlugin(), vuePlugin({ diff --git a/web_src/js/iife.ts b/web_src/js/iife.ts new file mode 100644 index 0000000000000..1c3987ff00622 --- /dev/null +++ b/web_src/js/iife.ts @@ -0,0 +1,11 @@ +// bootstrap module must be the first one to be imported, it handles global errors +import {initGlobalErrorHandler} from './bootstrap.ts'; + +// many users expect to use jQuery in their custom scripts (https://docs.gitea.com/administration/customizing-gitea#example-plantuml) +// so load globals (including jQuery) as early as possible +import './globals.ts'; + +import './webcomponents/index.ts'; +import './modules/user-settings.ts'; // templates also need to use localUserSettings in inline scripts + +initGlobalErrorHandler(); diff --git a/web_src/js/index-domready.ts b/web_src/js/index-domready.ts deleted file mode 100644 index e0b4a3e521afc..0000000000000 --- a/web_src/js/index-domready.ts +++ /dev/null @@ -1,188 +0,0 @@ -import '../fomantic/build/fomantic.js'; -import '../css/index.css'; -import type {HtmxResponseInfo} from 'htmx.org'; -import {showErrorToast} from './modules/toast.ts'; - -import {initDashboardRepoList} from './features/dashboard.ts'; -import {initGlobalCopyToClipboardListener} from './features/clipboard.ts'; -import {initRepoGraphGit} from './features/repo-graph.ts'; -import {initHeatmap} from './features/heatmap.ts'; -import {initImageDiff} from './features/imagediff.ts'; -import {initRepoMigration} from './features/repo-migration.ts'; -import {initRepoProject} from './features/repo-projects.ts'; -import {initTableSort} from './features/tablesort.ts'; -import {initAdminUserListSearchForm} from './features/admin/users.ts'; -import {initAdminConfigs} from './features/admin/config.ts'; -import {initMarkupAnchors} from './markup/anchors.ts'; -import {initNotificationCount} from './features/notification.ts'; -import {initRepoIssueContentHistory} from './features/repo-issue-content.ts'; -import {initStopwatch} from './features/stopwatch.ts'; -import {initRepoFileSearch} from './features/repo-findfile.ts'; -import {initMarkupContent} from './markup/content.ts'; -import {initRepoFileView} from './features/file-view.ts'; -import {initUserAuthOauth2, initUserCheckAppUrl} from './features/user-auth.ts'; -import {initRepoPullRequestAllowMaintainerEdit, initRepoPullRequestReview, initRepoIssueSidebarDependency, initRepoIssueFilterItemLabel} from './features/repo-issue.ts'; -import {initRepoEllipsisButton, initCommitStatuses} from './features/repo-commit.ts'; -import {initRepoTopicBar} from './features/repo-home.ts'; -import {initAdminCommon} from './features/admin/common.ts'; -import {initRepoCodeView} from './features/repo-code.ts'; -import {initSshKeyFormParser} from './features/sshkey-helper.ts'; -import {initUserSettings} from './features/user-settings.ts'; -import {initRepoActivityTopAuthorsChart, initRepoArchiveLinks} from './features/repo-common.ts'; -import {initRepoMigrationStatusChecker} from './features/repo-migrate.ts'; -import {initRepoDiffView} from './features/repo-diff.ts'; -import {initOrgTeam} from './features/org-team.ts'; -import {initUserAuthWebAuthn, initUserAuthWebAuthnRegister} from './features/user-auth-webauthn.ts'; -import {initRepoReleaseNew} from './features/repo-release.ts'; -import {initRepoEditor} from './features/repo-editor.ts'; -import {initCompSearchUserBox} from './features/comp/SearchUserBox.ts'; -import {initInstall} from './features/install.ts'; -import {initCompWebHookEditor} from './features/comp/WebHookEditor.ts'; -import {initRepoBranchButton} from './features/repo-branch.ts'; -import {initCommonOrganization} from './features/common-organization.ts'; -import {initRepoWikiForm} from './features/repo-wiki.ts'; -import {initRepository, initBranchSelectorTabs} from './features/repo-legacy.ts'; -import {initCopyContent} from './features/copycontent.ts'; -import {initCaptcha} from './features/captcha.ts'; -import {initRepositoryActionView} from './features/repo-actions.ts'; -import {initGlobalTooltips} from './modules/tippy.ts'; -import {initGiteaFomantic} from './modules/fomantic.ts'; -import {initSubmitEventPolyfill} from './utils/dom.ts'; -import {initRepoIssueList} from './features/repo-issue-list.ts'; -import {initCommonIssueListQuickGoto} from './features/common-issue-list.ts'; -import {initRepoContributors} from './features/contributors.ts'; -import {initRepoCodeFrequency} from './features/code-frequency.ts'; -import {initRepoRecentCommits} from './features/recent-commits.ts'; -import {initRepoDiffCommitBranchesAndTags} from './features/repo-diff-commit.ts'; -import {initGlobalSelectorObserver} from './modules/observer.ts'; -import {initRepositorySearch} from './features/repo-search.ts'; -import {initColorPickers} from './features/colorpicker.ts'; -import {initAdminSelfCheck} from './features/admin/selfcheck.ts'; -import {initOAuth2SettingsDisableCheckbox} from './features/oauth2-settings.ts'; -import {initGlobalFetchAction} from './features/common-fetch-action.ts'; -import {initCommmPageComponents, initGlobalComponent, initGlobalDropdown, initGlobalInput} from './features/common-page.ts'; -import {initGlobalButtonClickOnEnter, initGlobalButtons, initGlobalDeleteButton} from './features/common-button.ts'; -import {initGlobalComboMarkdownEditor, initGlobalEnterQuickSubmit, initGlobalFormDirtyLeaveConfirm} from './features/common-form.ts'; -import {callInitFunctions} from './modules/init.ts'; -import {initRepoViewFileTree} from './features/repo-view-file-tree.ts'; -import {initActionsPermissionsForm} from './features/common-actions-permissions.ts'; -import {initGlobalShortcut} from './modules/shortcut.ts'; - -const initStartTime = performance.now(); -const initPerformanceTracer = callInitFunctions([ - initSubmitEventPolyfill, - initGiteaFomantic, - - initGlobalComponent, - initGlobalDropdown, - initGlobalFetchAction, - initGlobalTooltips, - initGlobalButtonClickOnEnter, - initGlobalButtons, - initGlobalCopyToClipboardListener, - initGlobalEnterQuickSubmit, - initGlobalFormDirtyLeaveConfirm, - initGlobalComboMarkdownEditor, - initGlobalDeleteButton, - initGlobalInput, - initGlobalShortcut, - - initCommonOrganization, - initCommonIssueListQuickGoto, - - initCompSearchUserBox, - initCompWebHookEditor, - - initInstall, - - initCommmPageComponents, - - initHeatmap, - initImageDiff, - initMarkupAnchors, - initMarkupContent, - initSshKeyFormParser, - initStopwatch, - initTableSort, - initRepoFileSearch, - initCopyContent, - - initAdminCommon, - initAdminUserListSearchForm, - initAdminConfigs, - initAdminSelfCheck, - - initDashboardRepoList, - - initNotificationCount, - - initOrgTeam, - - initRepoActivityTopAuthorsChart, - initRepoArchiveLinks, - initRepoBranchButton, - initRepoCodeView, - initBranchSelectorTabs, - initRepoEllipsisButton, - initRepoDiffCommitBranchesAndTags, - initRepoEditor, - initRepoGraphGit, - initRepoIssueContentHistory, - initRepoIssueList, - initRepoIssueFilterItemLabel, - initRepoIssueSidebarDependency, - initRepoMigration, - initRepoMigrationStatusChecker, - initRepoProject, - initRepoPullRequestAllowMaintainerEdit, - initRepoPullRequestReview, - initRepoReleaseNew, - initRepoTopicBar, - initRepoViewFileTree, - initRepoWikiForm, - initRepository, - initRepositoryActionView, - initRepositorySearch, - initRepoContributors, - initRepoCodeFrequency, - initRepoRecentCommits, - - initCommitStatuses, - initCaptcha, - - initUserCheckAppUrl, - initUserAuthOauth2, - initUserAuthWebAuthn, - initUserAuthWebAuthnRegister, - initUserSettings, - initRepoDiffView, - initColorPickers, - - initOAuth2SettingsDisableCheckbox, - - initRepoFileView, - initActionsPermissionsForm, -]); - -// it must be the last one, then the "querySelectorAll" only needs to be executed once for global init functions. -initGlobalSelectorObserver(initPerformanceTracer); -if (initPerformanceTracer) initPerformanceTracer.printResults(); - -const initDur = performance.now() - initStartTime; -if (initDur > 500) { - console.error(`slow init functions took ${initDur.toFixed(3)}ms`); -} - -// https://htmx.org/events/#htmx:sendError -type HtmxEvent = Event & {detail: HtmxResponseInfo}; -document.body.addEventListener('htmx:sendError', (event) => { - // TODO: add translations - showErrorToast(`Network error when calling ${(event as HtmxEvent).detail.requestConfig.path}`); -}); -// https://htmx.org/events/#htmx:responseError -document.body.addEventListener('htmx:responseError', (event) => { - // TODO: add translations - showErrorToast(`Error ${(event as HtmxEvent).detail.xhr.status} when calling ${(event as HtmxEvent).detail.requestConfig.path}`); -}); - -document.dispatchEvent(new CustomEvent('gitea:index-ready')); diff --git a/web_src/js/index.ts b/web_src/js/index.ts index 1c3987ff00622..e0b4a3e521afc 100644 --- a/web_src/js/index.ts +++ b/web_src/js/index.ts @@ -1,11 +1,188 @@ -// bootstrap module must be the first one to be imported, it handles global errors -import {initGlobalErrorHandler} from './bootstrap.ts'; +import '../fomantic/build/fomantic.js'; +import '../css/index.css'; +import type {HtmxResponseInfo} from 'htmx.org'; +import {showErrorToast} from './modules/toast.ts'; -// many users expect to use jQuery in their custom scripts (https://docs.gitea.com/administration/customizing-gitea#example-plantuml) -// so load globals (including jQuery) as early as possible -import './globals.ts'; +import {initDashboardRepoList} from './features/dashboard.ts'; +import {initGlobalCopyToClipboardListener} from './features/clipboard.ts'; +import {initRepoGraphGit} from './features/repo-graph.ts'; +import {initHeatmap} from './features/heatmap.ts'; +import {initImageDiff} from './features/imagediff.ts'; +import {initRepoMigration} from './features/repo-migration.ts'; +import {initRepoProject} from './features/repo-projects.ts'; +import {initTableSort} from './features/tablesort.ts'; +import {initAdminUserListSearchForm} from './features/admin/users.ts'; +import {initAdminConfigs} from './features/admin/config.ts'; +import {initMarkupAnchors} from './markup/anchors.ts'; +import {initNotificationCount} from './features/notification.ts'; +import {initRepoIssueContentHistory} from './features/repo-issue-content.ts'; +import {initStopwatch} from './features/stopwatch.ts'; +import {initRepoFileSearch} from './features/repo-findfile.ts'; +import {initMarkupContent} from './markup/content.ts'; +import {initRepoFileView} from './features/file-view.ts'; +import {initUserAuthOauth2, initUserCheckAppUrl} from './features/user-auth.ts'; +import {initRepoPullRequestAllowMaintainerEdit, initRepoPullRequestReview, initRepoIssueSidebarDependency, initRepoIssueFilterItemLabel} from './features/repo-issue.ts'; +import {initRepoEllipsisButton, initCommitStatuses} from './features/repo-commit.ts'; +import {initRepoTopicBar} from './features/repo-home.ts'; +import {initAdminCommon} from './features/admin/common.ts'; +import {initRepoCodeView} from './features/repo-code.ts'; +import {initSshKeyFormParser} from './features/sshkey-helper.ts'; +import {initUserSettings} from './features/user-settings.ts'; +import {initRepoActivityTopAuthorsChart, initRepoArchiveLinks} from './features/repo-common.ts'; +import {initRepoMigrationStatusChecker} from './features/repo-migrate.ts'; +import {initRepoDiffView} from './features/repo-diff.ts'; +import {initOrgTeam} from './features/org-team.ts'; +import {initUserAuthWebAuthn, initUserAuthWebAuthnRegister} from './features/user-auth-webauthn.ts'; +import {initRepoReleaseNew} from './features/repo-release.ts'; +import {initRepoEditor} from './features/repo-editor.ts'; +import {initCompSearchUserBox} from './features/comp/SearchUserBox.ts'; +import {initInstall} from './features/install.ts'; +import {initCompWebHookEditor} from './features/comp/WebHookEditor.ts'; +import {initRepoBranchButton} from './features/repo-branch.ts'; +import {initCommonOrganization} from './features/common-organization.ts'; +import {initRepoWikiForm} from './features/repo-wiki.ts'; +import {initRepository, initBranchSelectorTabs} from './features/repo-legacy.ts'; +import {initCopyContent} from './features/copycontent.ts'; +import {initCaptcha} from './features/captcha.ts'; +import {initRepositoryActionView} from './features/repo-actions.ts'; +import {initGlobalTooltips} from './modules/tippy.ts'; +import {initGiteaFomantic} from './modules/fomantic.ts'; +import {initSubmitEventPolyfill} from './utils/dom.ts'; +import {initRepoIssueList} from './features/repo-issue-list.ts'; +import {initCommonIssueListQuickGoto} from './features/common-issue-list.ts'; +import {initRepoContributors} from './features/contributors.ts'; +import {initRepoCodeFrequency} from './features/code-frequency.ts'; +import {initRepoRecentCommits} from './features/recent-commits.ts'; +import {initRepoDiffCommitBranchesAndTags} from './features/repo-diff-commit.ts'; +import {initGlobalSelectorObserver} from './modules/observer.ts'; +import {initRepositorySearch} from './features/repo-search.ts'; +import {initColorPickers} from './features/colorpicker.ts'; +import {initAdminSelfCheck} from './features/admin/selfcheck.ts'; +import {initOAuth2SettingsDisableCheckbox} from './features/oauth2-settings.ts'; +import {initGlobalFetchAction} from './features/common-fetch-action.ts'; +import {initCommmPageComponents, initGlobalComponent, initGlobalDropdown, initGlobalInput} from './features/common-page.ts'; +import {initGlobalButtonClickOnEnter, initGlobalButtons, initGlobalDeleteButton} from './features/common-button.ts'; +import {initGlobalComboMarkdownEditor, initGlobalEnterQuickSubmit, initGlobalFormDirtyLeaveConfirm} from './features/common-form.ts'; +import {callInitFunctions} from './modules/init.ts'; +import {initRepoViewFileTree} from './features/repo-view-file-tree.ts'; +import {initActionsPermissionsForm} from './features/common-actions-permissions.ts'; +import {initGlobalShortcut} from './modules/shortcut.ts'; -import './webcomponents/index.ts'; -import './modules/user-settings.ts'; // templates also need to use localUserSettings in inline scripts +const initStartTime = performance.now(); +const initPerformanceTracer = callInitFunctions([ + initSubmitEventPolyfill, + initGiteaFomantic, -initGlobalErrorHandler(); + initGlobalComponent, + initGlobalDropdown, + initGlobalFetchAction, + initGlobalTooltips, + initGlobalButtonClickOnEnter, + initGlobalButtons, + initGlobalCopyToClipboardListener, + initGlobalEnterQuickSubmit, + initGlobalFormDirtyLeaveConfirm, + initGlobalComboMarkdownEditor, + initGlobalDeleteButton, + initGlobalInput, + initGlobalShortcut, + + initCommonOrganization, + initCommonIssueListQuickGoto, + + initCompSearchUserBox, + initCompWebHookEditor, + + initInstall, + + initCommmPageComponents, + + initHeatmap, + initImageDiff, + initMarkupAnchors, + initMarkupContent, + initSshKeyFormParser, + initStopwatch, + initTableSort, + initRepoFileSearch, + initCopyContent, + + initAdminCommon, + initAdminUserListSearchForm, + initAdminConfigs, + initAdminSelfCheck, + + initDashboardRepoList, + + initNotificationCount, + + initOrgTeam, + + initRepoActivityTopAuthorsChart, + initRepoArchiveLinks, + initRepoBranchButton, + initRepoCodeView, + initBranchSelectorTabs, + initRepoEllipsisButton, + initRepoDiffCommitBranchesAndTags, + initRepoEditor, + initRepoGraphGit, + initRepoIssueContentHistory, + initRepoIssueList, + initRepoIssueFilterItemLabel, + initRepoIssueSidebarDependency, + initRepoMigration, + initRepoMigrationStatusChecker, + initRepoProject, + initRepoPullRequestAllowMaintainerEdit, + initRepoPullRequestReview, + initRepoReleaseNew, + initRepoTopicBar, + initRepoViewFileTree, + initRepoWikiForm, + initRepository, + initRepositoryActionView, + initRepositorySearch, + initRepoContributors, + initRepoCodeFrequency, + initRepoRecentCommits, + + initCommitStatuses, + initCaptcha, + + initUserCheckAppUrl, + initUserAuthOauth2, + initUserAuthWebAuthn, + initUserAuthWebAuthnRegister, + initUserSettings, + initRepoDiffView, + initColorPickers, + + initOAuth2SettingsDisableCheckbox, + + initRepoFileView, + initActionsPermissionsForm, +]); + +// it must be the last one, then the "querySelectorAll" only needs to be executed once for global init functions. +initGlobalSelectorObserver(initPerformanceTracer); +if (initPerformanceTracer) initPerformanceTracer.printResults(); + +const initDur = performance.now() - initStartTime; +if (initDur > 500) { + console.error(`slow init functions took ${initDur.toFixed(3)}ms`); +} + +// https://htmx.org/events/#htmx:sendError +type HtmxEvent = Event & {detail: HtmxResponseInfo}; +document.body.addEventListener('htmx:sendError', (event) => { + // TODO: add translations + showErrorToast(`Network error when calling ${(event as HtmxEvent).detail.requestConfig.path}`); +}); +// https://htmx.org/events/#htmx:responseError +document.body.addEventListener('htmx:responseError', (event) => { + // TODO: add translations + showErrorToast(`Error ${(event as HtmxEvent).detail.xhr.status} when calling ${(event as HtmxEvent).detail.requestConfig.path}`); +}); + +document.dispatchEvent(new CustomEvent('gitea:index-ready')); From efb2a40eda79be4b34c5e885b69f5ae702aabda1 Mon Sep 17 00:00:00 2001 From: silverwind Date: Tue, 24 Mar 2026 02:56:42 +0100 Subject: [PATCH 061/102] Remove unnecessary renderBuiltUrl hook Vite 8 with base "./" computes correct relative paths for CSS asset references (e.g. ../fonts/) without needing the experimental renderBuiltUrl hook. Add a comment explaining the base option. Co-Authored-By: Claude (Opus 4.6) --- vite.config.ts | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/vite.config.ts b/vite.config.ts index f8661544f1b25..25b7c597fed27 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -120,7 +120,7 @@ function filterCssUrlPlugin(): Plugin { } export default defineConfig(commonViteOpts({ - base: './', + base: './', // make all asset URLs relative, so it works in subdirectory deployments build: { modulePreload: false, manifest: true, @@ -164,14 +164,6 @@ export default defineConfig(commonViteOpts({ ], }, }, - experimental: { - renderBuiltUrl(filename, {hostType}) { - if (hostType === 'css') { - return `../${filename}`; // CSS files are in css/, assets are siblings, so go up one level - } - return {relative: true}; - }, - }, define: { __VUE_OPTIONS_API__: true, __VUE_PROD_DEVTOOLS__: false, From 3952938341da30decf2c398cffdaaf21570b3f1b Mon Sep 17 00:00:00 2001 From: silverwind Date: Tue, 24 Mar 2026 03:03:00 +0100 Subject: [PATCH 062/102] Move base option into commonViteOpts Co-Authored-By: Claude (Opus 4.6) --- vite.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vite.config.ts b/vite.config.ts index 25b7c597fed27..738feb1e231f0 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -43,6 +43,7 @@ const commonRolldownOptions: Rolldown.RolldownOptions = { function commonViteOpts({build, ...other}: InlineConfig): InlineConfig { const {rolldownOptions, ...otherBuild} = build || {}; return { + base: './', // make all asset URLs relative, so it works in subdirectory deployments configFile: false, root: import.meta.dirname, publicDir: false, @@ -120,7 +121,6 @@ function filterCssUrlPlugin(): Plugin { } export default defineConfig(commonViteOpts({ - base: './', // make all asset URLs relative, so it works in subdirectory deployments build: { modulePreload: false, manifest: true, From 77a7e3141984a9b3b0a08ae47a66be4dcd1e2dc5 Mon Sep 17 00:00:00 2001 From: silverwind Date: Tue, 24 Mar 2026 03:06:44 +0100 Subject: [PATCH 063/102] Restore initGlobalErrorHandler as side effect in bootstrap.ts Move initGlobalErrorHandler() call back into bootstrap.ts so it runs as a side effect before other modules evaluate. This ensures errors in globals.ts, webcomponents, and user-settings.ts are caught. Co-Authored-By: Claude (Opus 4.6) --- web_src/js/bootstrap.ts | 4 +++- web_src/js/iife.ts | 4 +--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/web_src/js/bootstrap.ts b/web_src/js/bootstrap.ts index 7e474e26b0404..83f8383fe8e71 100644 --- a/web_src/js/bootstrap.ts +++ b/web_src/js/bootstrap.ts @@ -70,7 +70,7 @@ function processWindowErrorEvent({error, reason, message, type, filename, lineno showGlobalErrorMessage(`JavaScript ${renderedType}: ${msg}${dot} Open browser console to see more details.`); } -export function initGlobalErrorHandler() { +function initGlobalErrorHandler() { if (window._globalHandlerErrors?._inited) { showGlobalErrorMessage(`The global error handler has been initialized, do not initialize it again`); return; @@ -88,3 +88,5 @@ export function initGlobalErrorHandler() { // events directly window._globalHandlerErrors = {_inited: true, push: (e: ErrorEvent & PromiseRejectionEvent) => processWindowErrorEvent(e)} as any; } + +initGlobalErrorHandler(); diff --git a/web_src/js/iife.ts b/web_src/js/iife.ts index 1c3987ff00622..e76c91ccc84fe 100644 --- a/web_src/js/iife.ts +++ b/web_src/js/iife.ts @@ -1,5 +1,5 @@ // bootstrap module must be the first one to be imported, it handles global errors -import {initGlobalErrorHandler} from './bootstrap.ts'; +import './bootstrap.ts'; // many users expect to use jQuery in their custom scripts (https://docs.gitea.com/administration/customizing-gitea#example-plantuml) // so load globals (including jQuery) as early as possible @@ -7,5 +7,3 @@ import './globals.ts'; import './webcomponents/index.ts'; import './modules/user-settings.ts'; // templates also need to use localUserSettings in inline scripts - -initGlobalErrorHandler(); From 0c625ab03ea6d2cb0e91af260a138ffe5dd72831 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Tue, 24 Mar 2026 10:13:41 +0800 Subject: [PATCH 064/102] Update services/webtheme/webtheme.go Signed-off-by: wxiaoguang --- services/webtheme/webtheme.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/services/webtheme/webtheme.go b/services/webtheme/webtheme.go index 6f2767da6641d..b69fd9858ebb5 100644 --- a/services/webtheme/webtheme.go +++ b/services/webtheme/webtheme.go @@ -110,6 +110,8 @@ func parseThemeMetaInfoToMap(cssContent string) map[string]string { // stripContentHash removes a Vite content hash suffix from a name. // e.g. "gitea-dark.CyAaQnn5" -> "gitea-dark" +// It might be wrong when user's theme name is like "my-theme-1.2.css", fortunately it is not a serious problem at the moment +// If we'd like to "fix" it, we can add a "hash prefix" to the Vite assets like "index.h~123456.css", then in most cases we do best guess to strip the hash correctly. func stripContentHash(name string) string { if i := strings.LastIndex(name, "."); i > 0 { return name[:i] From 8146f74a146b0c190d14eddc25a0b36e2352d990 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Tue, 24 Mar 2026 10:37:12 +0800 Subject: [PATCH 065/102] fine tune iife and global error handling --- web_src/js/bootstrap.ts | 4 +++- web_src/js/iife.ts | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/web_src/js/bootstrap.ts b/web_src/js/bootstrap.ts index 83f8383fe8e71..e5f707161ccff 100644 --- a/web_src/js/bootstrap.ts +++ b/web_src/js/bootstrap.ts @@ -72,7 +72,9 @@ function processWindowErrorEvent({error, reason, message, type, filename, lineno function initGlobalErrorHandler() { if (window._globalHandlerErrors?._inited) { - showGlobalErrorMessage(`The global error handler has been initialized, do not initialize it again`); + // when using vite rolldown in this project, this module will be loaded twice: + // * first time in the "iife.ts" + // * then as ES module by "import {showGlobalErrorMessage} ..." return; } if (!window.config) { diff --git a/web_src/js/iife.ts b/web_src/js/iife.ts index e76c91ccc84fe..218519c59a3d1 100644 --- a/web_src/js/iife.ts +++ b/web_src/js/iife.ts @@ -1,3 +1,5 @@ +// This file is the entry point for the code which should block the page rendering, it is compiled by our "iife" vite plugin + // bootstrap module must be the first one to be imported, it handles global errors import './bootstrap.ts'; From e81cda97abe21dd2ce6a9affe460fbea2395067e Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Tue, 24 Mar 2026 11:05:20 +0800 Subject: [PATCH 066/102] show the importance of "do not import a module twice" --- web_src/js/bootstrap.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/web_src/js/bootstrap.ts b/web_src/js/bootstrap.ts index e5f707161ccff..b5ba4476817f7 100644 --- a/web_src/js/bootstrap.ts +++ b/web_src/js/bootstrap.ts @@ -72,9 +72,10 @@ function processWindowErrorEvent({error, reason, message, type, filename, lineno function initGlobalErrorHandler() { if (window._globalHandlerErrors?._inited) { - // when using vite rolldown in this project, this module will be loaded twice: - // * first time in the "iife.ts" - // * then as ES module by "import {showGlobalErrorMessage} ..." + // A module should not be imported twice, otherwise there will be bugs when a module has its internal states. + // A real example is "generateElemId" in "utils/dom.ts", if it is imported twice in different module scopes, + // It will generate duplicate IDs (ps: don't try to use "random" to fix, it is just a real example to show the importance of "do not import a module twice") + showGlobalErrorMessage(`The global error handler has been initialized, do not initialize it again`); return; } if (!window.config) { From 88ac604c809a8f4496174c38709c81f847de8a76 Mon Sep 17 00:00:00 2001 From: silverwind Date: Tue, 24 Mar 2026 14:09:17 +0100 Subject: [PATCH 067/102] Revert default `STATIC_CACHE_TIME` back to 6h The 720h default is too aggressive for non-hashed assets like avatars and custom files. A targeted long cache for content-hashed manifest assets can be added separately. Co-Authored-By: Claude (Opus 4.6) --- custom/conf/app.example.ini | 4 ++-- modules/setting/server.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index a1c2b1eefaf74..b752a81ca934a 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -341,8 +341,8 @@ RUN_USER = ; git ;; 0 disables this. ;STARTUP_TIMEOUT = 0 ;; -;; Static resources, includes resources on custom/, public/ and all uploaded avatars web browser cache time. Note that this cache is disabled when RUN_MODE is "dev". Default is 720h (30 days) -;STATIC_CACHE_TIME = 720h +;; Static resources, includes resources on custom/, public/ and all uploaded avatars web browser cache time. Note that this cache is disabled when RUN_MODE is "dev". Default is 6h +;STATIC_CACHE_TIME = 6h ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/modules/setting/server.go b/modules/setting/server.go index cace563a6a81b..1085e052a3e6c 100644 --- a/modules/setting/server.go +++ b/modules/setting/server.go @@ -345,7 +345,7 @@ func loadServerFrom(rootCfg ConfigProvider) { StaticRootPath = AppWorkPath } StaticRootPath = sec.Key("STATIC_ROOT_PATH").MustString(StaticRootPath) - StaticCacheTime = sec.Key("STATIC_CACHE_TIME").MustDuration(720 * time.Hour) + StaticCacheTime = sec.Key("STATIC_CACHE_TIME").MustDuration(6 * time.Hour) AppDataPath = sec.Key("APP_DATA_PATH").MustString(filepath.Join(AppWorkPath, "data")) if !filepath.IsAbs(AppDataPath) { AppDataPath = filepath.ToSlash(filepath.Join(AppWorkPath, AppDataPath)) From 6a4b3eff0d8b344cbbb144d99bdb40879803af22 Mon Sep 17 00:00:00 2001 From: silverwind Date: Tue, 24 Mar 2026 14:49:19 +0100 Subject: [PATCH 068/102] Replace tippy.js with custom popup in overflow-menu Remove tippy.js and @popperjs/core dependencies from the iife bundle by replacing the overflow-menu dropdown with a lightweight custom popup using absolute positioning, CSS border-triangle arrow, and native click-outside/keyboard handling. This reduces the iife bundle by ~97 KB (uncompressed). Co-Authored-By: Claude (Opus 4.6) --- web_src/css/base.css | 52 +++++++++ web_src/js/webcomponents/overflow-menu.ts | 123 +++++++++++++--------- 2 files changed, 125 insertions(+), 50 deletions(-) diff --git a/web_src/css/base.css b/web_src/css/base.css index 2c7bd7395a49a..264ff5c32e8da 100644 --- a/web_src/css/base.css +++ b/web_src/css/base.css @@ -544,6 +544,58 @@ strong.attention-caution, svg.attention-caution { overflow-menu { border-bottom: 1px solid var(--color-secondary) !important; display: flex; + position: relative; +} + +overflow-menu .overflow-menu-popup { + position: absolute; + top: calc(100% + 8px); + right: 0; + z-index: 100; + background-color: var(--color-menu); + color: var(--color-text); + border: 1px solid var(--color-secondary); + border-radius: var(--border-radius); + box-shadow: 0 6px 18px var(--color-shadow); + padding: 4px 0; +} + +overflow-menu .overflow-menu-popup::before, +overflow-menu .overflow-menu-popup::after { + content: ""; + position: absolute; + right: 10px; + border: 8px solid transparent; +} + +overflow-menu .overflow-menu-popup::before { + bottom: 100%; + border-bottom-color: var(--color-secondary); +} + +overflow-menu .overflow-menu-popup::after { + bottom: calc(100% - 1px); + border-bottom-color: var(--color-menu); +} + +overflow-menu .overflow-menu-popup > .item { + display: flex; + align-items: center; + padding: 9px 18px; + color: inherit; + background: inherit; + text-decoration: none; + gap: 10px; + width: 100%; +} + +overflow-menu .overflow-menu-popup > .item:hover, +overflow-menu .overflow-menu-popup > .item:focus { + background: var(--color-hover); +} + +overflow-menu .overflow-menu-popup > .item.active { + background: var(--color-active); } overflow-menu .overflow-menu-items { diff --git a/web_src/js/webcomponents/overflow-menu.ts b/web_src/js/webcomponents/overflow-menu.ts index 1ddd984c12cb1..879ce36e6e309 100644 --- a/web_src/js/webcomponents/overflow-menu.ts +++ b/web_src/js/webcomponents/overflow-menu.ts @@ -1,11 +1,10 @@ import {throttle} from 'throttle-debounce'; -import {createTippy} from '../modules/tippy.ts'; -import {addDelegatedEventListener, isDocumentFragmentOrElementNode} from '../utils/dom.ts'; +import {addDelegatedEventListener, generateElemId, isDocumentFragmentOrElementNode} from '../utils/dom.ts'; import octiconKebabHorizontal from '../../../public/assets/img/svg/octicon-kebab-horizontal.svg'; window.customElements.define('overflow-menu', class extends HTMLElement { - tippyContent: HTMLDivElement; - tippyItems: Array; + popup: HTMLDivElement; + overflowItems: Array; button: HTMLButtonElement | null; menuItemsEl: HTMLElement; resizeObserver: ResizeObserver; @@ -13,18 +12,42 @@ window.customElements.define('overflow-menu', class extends HTMLElement { lastWidth: number; updateButtonActivationState() { - if (!this.button || !this.tippyContent) return; - this.button.classList.toggle('active', Boolean(this.tippyContent.querySelector('.item.active'))); + if (!this.button || !this.popup) return; + this.button.classList.toggle('active', Boolean(this.popup.querySelector('.item.active'))); } + showPopup() { + if (!this.popup || this.popup.style.display !== 'none') return; + this.popup.style.display = ''; + this.button!.setAttribute('aria-expanded', 'true'); + setTimeout(() => this.popup.focus(), 0); + document.addEventListener('click', this.onClickOutside, true); + } + + hidePopup() { + if (!this.popup || this.popup.style.display === 'none') return; + this.popup.style.display = 'none'; + this.button?.setAttribute('aria-expanded', 'false'); + document.removeEventListener('click', this.onClickOutside, true); + } + + onClickOutside = (e: Event) => { + if (!this.popup?.contains(e.target as Node) && !this.button?.contains(e.target as Node)) { + this.hidePopup(); + } + }; + updateItems = throttle(100, () => { - if (!this.tippyContent) { + if (!this.popup) { const div = document.createElement('div'); + div.classList.add('overflow-menu-popup'); + div.setAttribute('role', 'menu'); div.tabIndex = -1; // for initial focus, programmatic focus only + div.style.display = 'none'; div.addEventListener('keydown', (e) => { if (e.isComposing) return; if (e.key === 'Tab') { - const items = this.tippyContent.querySelectorAll('[role="menuitem"]'); + const items = this.popup.querySelectorAll('[role="menuitem"]'); if (e.shiftKey) { if (document.activeElement === items[0]) { e.preventDefault(); @@ -39,7 +62,7 @@ window.customElements.define('overflow-menu', class extends HTMLElement { } else if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); - this.button?._tippy.hide(); + this.hidePopup(); this.button?.focus(); } else if (e.key === ' ' || e.code === 'Enter') { if (document.activeElement?.matches('[role="menuitem"]')) { @@ -48,20 +71,20 @@ window.customElements.define('overflow-menu', class extends HTMLElement { (document.activeElement as HTMLElement).click(); } } else if (e.key === 'ArrowDown') { - if (document.activeElement?.matches('.tippy-target')) { + if (document.activeElement === this.popup) { e.preventDefault(); e.stopPropagation(); - document.activeElement.querySelector('[role="menuitem"]:first-of-type')?.focus(); + this.popup.querySelector('[role="menuitem"]:first-of-type')?.focus(); } else if (document.activeElement?.matches('[role="menuitem"]')) { e.preventDefault(); e.stopPropagation(); (document.activeElement.nextElementSibling as HTMLElement)?.focus(); } } else if (e.key === 'ArrowUp') { - if (document.activeElement?.matches('.tippy-target')) { + if (document.activeElement === this.popup) { e.preventDefault(); e.stopPropagation(); - document.activeElement.querySelector('[role="menuitem"]:last-of-type')?.focus(); + this.popup.querySelector('[role="menuitem"]:last-of-type')?.focus(); } else if (document.activeElement?.matches('[role="menuitem"]')) { e.preventDefault(); e.stopPropagation(); @@ -69,16 +92,15 @@ window.customElements.define('overflow-menu', class extends HTMLElement { } } }); - div.classList.add('tippy-target'); - this.handleItemClick(div, '.tippy-target > .item'); - this.tippyContent = div; - } // end if: no tippyContent and create a new one + this.handleItemClick(div, '.overflow-menu-popup > .item'); + this.popup = div; + } // end if: no popup and create a new one const itemFlexSpace = this.menuItemsEl.querySelector('.item-flex-space'); const itemOverFlowMenuButton = this.querySelector('.overflow-menu-button'); - // move items in tippy back into the menu items for subsequent measurement - for (const item of this.tippyItems || []) { + // move items in popup back into the menu items for subsequent measurement + for (const item of this.overflowItems || []) { if (!itemFlexSpace || item.getAttribute('data-after-flex-space')) { this.menuItemsEl.append(item); } else { @@ -90,7 +112,7 @@ window.customElements.define('overflow-menu', class extends HTMLElement { // flex space and overflow menu are excluded from measurement itemFlexSpace?.style.setProperty('display', 'none', 'important'); itemOverFlowMenuButton?.style.setProperty('display', 'none', 'important'); - this.tippyItems = []; + this.overflowItems = []; const menuRight = this.offsetLeft + this.offsetWidth; const menuItems = this.menuItemsEl.querySelectorAll('.item, .item-flex-space'); let afterFlexSpace = false; @@ -102,64 +124,64 @@ window.customElements.define('overflow-menu', class extends HTMLElement { if (afterFlexSpace) item.setAttribute('data-after-flex-space', 'true'); const itemRight = item.offsetLeft + item.offsetWidth; if (menuRight - itemRight < 38) { // roughly the width of .overflow-menu-button with some extra space - const onlyLastItem = idx === menuItems.length - 1 && this.tippyItems.length === 0; + const onlyLastItem = idx === menuItems.length - 1 && this.overflowItems.length === 0; const lastItemFit = onlyLastItem && menuRight - itemRight > 0; const moveToPopup = !onlyLastItem || !lastItemFit; - if (moveToPopup) this.tippyItems.push(item); + if (moveToPopup) this.overflowItems.push(item); } } itemFlexSpace?.style.removeProperty('display'); itemOverFlowMenuButton?.style.removeProperty('display'); // if there are no overflown items, remove any previously created button - if (!this.tippyItems?.length) { - const btn = this.querySelector('.overflow-menu-button'); - btn?._tippy?.destroy(); - btn?.remove(); + if (!this.overflowItems?.length) { + this.hidePopup(); + this.button?.remove(); + this.popup?.remove(); this.button = null; return; } - // remove aria role from items that moved from tippy to menu + // remove aria role from items that moved from popup to menu for (const item of menuItems) { - if (!this.tippyItems.includes(item)) { + if (!this.overflowItems.includes(item)) { item.removeAttribute('role'); } } - // move all items that overflow into tippy - for (const item of this.tippyItems) { + // move all items that overflow into popup + for (const item of this.overflowItems) { item.setAttribute('role', 'menuitem'); - this.tippyContent.append(item); + this.popup.append(item); } - // update existing tippy - if (this.button?._tippy) { - this.button._tippy.setContent(this.tippyContent); + // update existing popup + if (this.button) { this.updateButtonActivationState(); return; } - // create button initially + // create button and attach popup + const popupId = generateElemId('overflow-popup-'); + this.popup.id = popupId; + this.button = document.createElement('button'); this.button.classList.add('overflow-menu-button'); this.button.setAttribute('aria-label', window.config.i18n.more_items); + this.button.setAttribute('aria-haspopup', 'true'); + this.button.setAttribute('aria-expanded', 'false'); + this.button.setAttribute('aria-controls', popupId); this.button.innerHTML = octiconKebabHorizontal; - this.append(this.button); - createTippy(this.button, { - trigger: 'click', - hideOnClick: true, - interactive: true, - placement: 'bottom-end', - role: 'menu', - theme: 'menu', - content: this.tippyContent, - onShow: () => { // FIXME: onShown doesn't work (never be called) - setTimeout(() => { - this.tippyContent.focus(); - }, 0); - }, + this.button.addEventListener('click', (e) => { + e.stopPropagation(); + if (this.popup.style.display === 'none') { + this.showPopup(); + } else { + this.hidePopup(); + } }); + this.append(this.button); + this.append(this.popup); this.updateButtonActivationState(); }); @@ -202,7 +224,7 @@ window.customElements.define('overflow-menu', class extends HTMLElement { handleItemClick(el: Element, selector: string) { addDelegatedEventListener(el, 'click', selector, () => { - this.button?._tippy?.hide(); + this.hidePopup(); this.updateButtonActivationState(); }); } @@ -239,5 +261,6 @@ window.customElements.define('overflow-menu', class extends HTMLElement { disconnectedCallback() { this.mutationObserver?.disconnect(); this.resizeObserver?.disconnect(); + document.removeEventListener('click', this.onClickOutside, true); } }); From b7a2b0ae516c102fc60a34cb8c06514c92bc700c Mon Sep 17 00:00:00 2001 From: silverwind Date: Tue, 24 Mar 2026 15:06:43 +0100 Subject: [PATCH 069/102] Extract `showGlobalErrorMessage` to `modules/message.ts` Move the function out of bootstrap.ts into a side-effect-free module so the ESM bundle can import it without re-triggering the global error handler initialization. Fix overflow-menu popup item styling to override fomantic specificity. Co-Authored-By: Claude (Opus 4.6) --- web_src/css/base.css | 10 +++++----- web_src/js/bootstrap.test.ts | 19 ++++--------------- web_src/js/bootstrap.ts | 25 +------------------------ web_src/js/features/common-page.ts | 2 +- web_src/js/modules/message.test.ts | 12 ++++++++++++ web_src/js/modules/message.ts | 23 +++++++++++++++++++++++ 6 files changed, 46 insertions(+), 45 deletions(-) create mode 100644 web_src/js/modules/message.test.ts create mode 100644 web_src/js/modules/message.ts diff --git a/web_src/css/base.css b/web_src/css/base.css index 264ff5c32e8da..eeb9e35443229 100644 --- a/web_src/css/base.css +++ b/web_src/css/base.css @@ -581,9 +581,9 @@ overflow-menu .overflow-menu-popup::after { overflow-menu .overflow-menu-popup > .item { display: flex; align-items: center; - padding: 9px 18px; - color: inherit; - background: inherit; + padding: 9px 18px !important; + color: var(--color-text) !important; + background: transparent !important; text-decoration: none; gap: 10px; width: 100%; @@ -591,11 +591,11 @@ overflow-menu .overflow-menu-popup > .item { overflow-menu .overflow-menu-popup > .item:hover, overflow-menu .overflow-menu-popup > .item:focus { - background: var(--color-hover); + background: var(--color-hover) !important; } overflow-menu .overflow-menu-popup > .item.active { - background: var(--color-active); + background: var(--color-active) !important; } overflow-menu .overflow-menu-items { diff --git a/web_src/js/bootstrap.test.ts b/web_src/js/bootstrap.test.ts index 9d163ebbb8350..e9fa24cc4a22a 100644 --- a/web_src/js/bootstrap.test.ts +++ b/web_src/js/bootstrap.test.ts @@ -1,21 +1,10 @@ -import {showGlobalErrorMessage, shouldIgnoreError} from './bootstrap.ts'; - -test('showGlobalErrorMessage', () => { - document.body.innerHTML = '
'; - showGlobalErrorMessage('test msg 1'); - showGlobalErrorMessage('test msg 2'); - showGlobalErrorMessage('test msg 1'); // duplicated - - expect(document.body.innerHTML).toContain('>test msg 1 (2)<'); - expect(document.body.innerHTML).toContain('>test msg 2<'); - expect(document.querySelectorAll('.js-global-error').length).toEqual(2); -}); +import {shouldIgnoreError} from './bootstrap.ts'; test('shouldIgnoreError', () => { for (const url of [ - 'https://gitea.test/assets/js/monaco.b359ef7e.js', - 'https://gitea.test/assets/js/monaco-editor.4a969118.worker.js', - 'https://gitea.test/assets/js/vendors-node_modules_pnpm_monaco-editor_0_55_1_node_modules_monaco-editor_esm_vs_base_common_-e11c7c.966a028d.js', + 'https://gitea.test/assets/js/monaco.D14TzjS9.js', + 'https://gitea.test/assets/js/editor.api2.BdhK7zNg.js', + 'https://gitea.test/assets/js/editor.worker.BYgvyFya.js', ]) { const err = new Error('test'); err.stack = `Error: test\n at ${url}:1:1`; diff --git a/web_src/js/bootstrap.ts b/web_src/js/bootstrap.ts index b5ba4476817f7..46419000b73ab 100644 --- a/web_src/js/bootstrap.ts +++ b/web_src/js/bootstrap.ts @@ -1,8 +1,7 @@ // DO NOT IMPORT window.config HERE! // to make sure the error handler always works, we should never import `window.config`, because // some user's custom template breaks it. -import type {Intent} from './types.ts'; -import {html} from './utils/html.ts'; +import {showGlobalErrorMessage} from './modules/message.ts'; // This file must be imported before any lazy-loading is being attempted. @@ -19,27 +18,6 @@ export function shouldIgnoreError(err: Error) { return false; } -export function showGlobalErrorMessage(msg: string, msgType: Intent = 'error') { - const msgContainer = document.querySelector('.page-content') ?? document.body; - if (!msgContainer) { - alert(`${msgType}: ${msg}`); - return; - } - const msgCompact = msg.replace(/\W/g, '').trim(); // compact the message to a data attribute to avoid too many duplicated messages - let msgDiv = msgContainer.querySelector(`.js-global-error[data-global-error-msg-compact="${msgCompact}"]`); - if (!msgDiv) { - const el = document.createElement('div'); - el.innerHTML = html`
`; - msgDiv = el.childNodes[0] as HTMLDivElement; - } - // merge duplicated messages into "the message (count)" format - const msgCount = Number(msgDiv.getAttribute(`data-global-error-msg-count`)) + 1; - msgDiv.setAttribute(`data-global-error-msg-compact`, msgCompact); - msgDiv.setAttribute(`data-global-error-msg-count`, msgCount.toString()); - msgDiv.querySelector('.ui.message')!.textContent = msg + (msgCount > 1 ? ` (${msgCount})` : ''); - msgContainer.prepend(msgDiv); -} - function processWindowErrorEvent({error, reason, message, type, filename, lineno, colno}: ErrorEvent & PromiseRejectionEvent) { const err = error ?? reason; const assetBaseUrl = String(new URL(`${window.config?.assetUrlPrefix ?? '/assets'}/`, window.location.origin)); @@ -75,7 +53,6 @@ function initGlobalErrorHandler() { // A module should not be imported twice, otherwise there will be bugs when a module has its internal states. // A real example is "generateElemId" in "utils/dom.ts", if it is imported twice in different module scopes, // It will generate duplicate IDs (ps: don't try to use "random" to fix, it is just a real example to show the importance of "do not import a module twice") - showGlobalErrorMessage(`The global error handler has been initialized, do not initialize it again`); return; } if (!window.config) { diff --git a/web_src/js/features/common-page.ts b/web_src/js/features/common-page.ts index 36af087089998..ed6679506ff60 100644 --- a/web_src/js/features/common-page.ts +++ b/web_src/js/features/common-page.ts @@ -1,5 +1,5 @@ import {GET, POST} from '../modules/fetch.ts'; -import {showGlobalErrorMessage} from '../bootstrap.ts'; +import {showGlobalErrorMessage} from '../modules/message.ts'; import {fomanticQuery} from '../modules/fomantic/base.ts'; import {addDelegatedEventListener, queryElems} from '../utils/dom.ts'; import {registerGlobalInitFunc, registerGlobalSelectorFunc} from '../modules/observer.ts'; diff --git a/web_src/js/modules/message.test.ts b/web_src/js/modules/message.test.ts new file mode 100644 index 0000000000000..b002ae49af19a --- /dev/null +++ b/web_src/js/modules/message.test.ts @@ -0,0 +1,12 @@ +import {showGlobalErrorMessage} from './message.ts'; + +test('showGlobalErrorMessage', () => { + document.body.innerHTML = '
'; + showGlobalErrorMessage('test msg 1'); + showGlobalErrorMessage('test msg 2'); + showGlobalErrorMessage('test msg 1'); // duplicated + + expect(document.body.innerHTML).toContain('>test msg 1 (2)<'); + expect(document.body.innerHTML).toContain('>test msg 2<'); + expect(document.querySelectorAll('.js-global-error').length).toEqual(2); +}); diff --git a/web_src/js/modules/message.ts b/web_src/js/modules/message.ts new file mode 100644 index 0000000000000..1e927b0c1bc08 --- /dev/null +++ b/web_src/js/modules/message.ts @@ -0,0 +1,23 @@ +import type {Intent} from '../types.ts'; +import {html} from '../utils/html.ts'; + +export function showGlobalErrorMessage(msg: string, msgType: Intent = 'error') { + const msgContainer = document.querySelector('.page-content') ?? document.body; + if (!msgContainer) { + alert(`${msgType}: ${msg}`); + return; + } + const msgCompact = msg.replace(/\W/g, '').trim(); // compact the message to a data attribute to avoid too many duplicated messages + let msgDiv = msgContainer.querySelector(`.js-global-error[data-global-error-msg-compact="${msgCompact}"]`); + if (!msgDiv) { + const el = document.createElement('div'); + el.innerHTML = html`
`; + msgDiv = el.childNodes[0] as HTMLDivElement; + } + // merge duplicated messages into "the message (count)" format + const msgCount = Number(msgDiv.getAttribute(`data-global-error-msg-count`)) + 1; + msgDiv.setAttribute(`data-global-error-msg-compact`, msgCompact); + msgDiv.setAttribute(`data-global-error-msg-count`, msgCount.toString()); + msgDiv.querySelector('.ui.message')!.textContent = msg + (msgCount > 1 ? ` (${msgCount})` : ''); + msgContainer.prepend(msgDiv); +} From 46f6a7ead72d4d22659ee203187a26a4911fe6b5 Mon Sep 17 00:00:00 2001 From: silverwind Date: Tue, 24 Mar 2026 15:20:41 +0100 Subject: [PATCH 070/102] Move error handling functions to `modules/errors.ts` Extract showGlobalErrorMessage, shouldIgnoreError, and processWindowErrorEvent into a side-effect-free module. This keeps bootstrap.ts minimal (just the init guard and side effect) and prevents the ESM bundle from re-triggering the global error handler. Co-Authored-By: Claude (Opus 4.6) --- web_src/js/bootstrap.ts | 60 ++--------------- web_src/js/features/common-page.ts | 2 +- .../errors.test.ts} | 13 +++- web_src/js/modules/errors.ts | 67 +++++++++++++++++++ web_src/js/modules/message.test.ts | 12 ---- web_src/js/modules/message.ts | 23 ------- 6 files changed, 85 insertions(+), 92 deletions(-) rename web_src/js/{bootstrap.test.ts => modules/errors.test.ts} (52%) create mode 100644 web_src/js/modules/errors.ts delete mode 100644 web_src/js/modules/message.test.ts delete mode 100644 web_src/js/modules/message.ts diff --git a/web_src/js/bootstrap.ts b/web_src/js/bootstrap.ts index 46419000b73ab..f88f4900637cc 100644 --- a/web_src/js/bootstrap.ts +++ b/web_src/js/bootstrap.ts @@ -1,60 +1,12 @@ // DO NOT IMPORT window.config HERE! // to make sure the error handler always works, we should never import `window.config`, because // some user's custom template breaks it. -import {showGlobalErrorMessage} from './modules/message.ts'; +import {showGlobalErrorMessage, processWindowErrorEvent} from './modules/errors.ts'; -// This file must be imported before any lazy-loading is being attempted. - -export function shouldIgnoreError(err: Error) { - const ignorePatterns: Array = [ - // https://github.com/go-gitea/gitea/issues/30861 - // https://github.com/microsoft/monaco-editor/issues/4496 - // https://github.com/microsoft/monaco-editor/issues/4679 - /\/assets\/js\/.*(monaco|editor\.(api|worker))/, - ]; - for (const pattern of ignorePatterns) { - if (pattern.test(err.stack ?? '')) return true; - } - return false; -} - -function processWindowErrorEvent({error, reason, message, type, filename, lineno, colno}: ErrorEvent & PromiseRejectionEvent) { - const err = error ?? reason; - const assetBaseUrl = String(new URL(`${window.config?.assetUrlPrefix ?? '/assets'}/`, window.location.origin)); - const {runModeIsProd} = window.config ?? {}; - - // `error` and `reason` are not guaranteed to be errors. If the value is falsy, it is likely a - // non-critical event from the browser. We log them but don't show them to users. Examples: - // - https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver#observation_errors - // - https://github.com/mozilla-mobile/firefox-ios/issues/10817 - // - https://github.com/go-gitea/gitea/issues/20240 - if (!err) { - if (message) console.error(new Error(message)); - if (runModeIsProd) return; - } - - if (err instanceof Error) { - // If the error stack trace does not include the base URL of our script assets, it likely came - // from a browser extension or inline script. Do not show such errors in production. - if (!err.stack?.includes(assetBaseUrl) && runModeIsProd) return; - // Ignore some known errors that are unable to fix - if (shouldIgnoreError(err)) return; - } - - let msg = err?.message ?? message; - if (lineno) msg += ` (${filename} @ ${lineno}:${colno})`; - const dot = msg.endsWith('.') ? '' : '.'; - const renderedType = type === 'unhandledrejection' ? 'promise rejection' : type; - showGlobalErrorMessage(`JavaScript ${renderedType}: ${msg}${dot} Open browser console to see more details.`); -} - -function initGlobalErrorHandler() { - if (window._globalHandlerErrors?._inited) { - // A module should not be imported twice, otherwise there will be bugs when a module has its internal states. - // A real example is "generateElemId" in "utils/dom.ts", if it is imported twice in different module scopes, - // It will generate duplicate IDs (ps: don't try to use "random" to fix, it is just a real example to show the importance of "do not import a module twice") - return; - } +// A module should not be imported twice, otherwise there will be bugs when a module has its internal states. +// A real example is "generateElemId" in "utils/dom.ts", if it is imported twice in different module scopes, +// It will generate duplicate IDs (ps: don't try to use "random" to fix, it is just a real example to show the importance of "do not import a module twice") +if (!window._globalHandlerErrors?._inited) { if (!window.config) { showGlobalErrorMessage(`Gitea JavaScript code couldn't run correctly, please check your custom templates`); } @@ -68,5 +20,3 @@ function initGlobalErrorHandler() { // events directly window._globalHandlerErrors = {_inited: true, push: (e: ErrorEvent & PromiseRejectionEvent) => processWindowErrorEvent(e)} as any; } - -initGlobalErrorHandler(); diff --git a/web_src/js/features/common-page.ts b/web_src/js/features/common-page.ts index ed6679506ff60..fd37e307f766d 100644 --- a/web_src/js/features/common-page.ts +++ b/web_src/js/features/common-page.ts @@ -1,5 +1,5 @@ import {GET, POST} from '../modules/fetch.ts'; -import {showGlobalErrorMessage} from '../modules/message.ts'; +import {showGlobalErrorMessage} from '../modules/errors.ts'; import {fomanticQuery} from '../modules/fomantic/base.ts'; import {addDelegatedEventListener, queryElems} from '../utils/dom.ts'; import {registerGlobalInitFunc, registerGlobalSelectorFunc} from '../modules/observer.ts'; diff --git a/web_src/js/bootstrap.test.ts b/web_src/js/modules/errors.test.ts similarity index 52% rename from web_src/js/bootstrap.test.ts rename to web_src/js/modules/errors.test.ts index e9fa24cc4a22a..c860a3f7cb11d 100644 --- a/web_src/js/bootstrap.test.ts +++ b/web_src/js/modules/errors.test.ts @@ -1,4 +1,15 @@ -import {shouldIgnoreError} from './bootstrap.ts'; +import {showGlobalErrorMessage, shouldIgnoreError} from './errors.ts'; + +test('showGlobalErrorMessage', () => { + document.body.innerHTML = '
'; + showGlobalErrorMessage('test msg 1'); + showGlobalErrorMessage('test msg 2'); + showGlobalErrorMessage('test msg 1'); // duplicated + + expect(document.body.innerHTML).toContain('>test msg 1 (2)<'); + expect(document.body.innerHTML).toContain('>test msg 2<'); + expect(document.querySelectorAll('.js-global-error').length).toEqual(2); +}); test('shouldIgnoreError', () => { for (const url of [ diff --git a/web_src/js/modules/errors.ts b/web_src/js/modules/errors.ts new file mode 100644 index 0000000000000..3ec01b3eb7c02 --- /dev/null +++ b/web_src/js/modules/errors.ts @@ -0,0 +1,67 @@ +// keep this file lightweight, it's imported into IIFE chunk in bootstrap +import {html} from '../utils/html.ts'; +import type {Intent} from '../types.ts'; + +export function showGlobalErrorMessage(msg: string, msgType: Intent = 'error') { + const msgContainer = document.querySelector('.page-content') ?? document.body; + if (!msgContainer) { + alert(`${msgType}: ${msg}`); + return; + } + const msgCompact = msg.replace(/\W/g, '').trim(); // compact the message to a data attribute to avoid too many duplicated messages + let msgDiv = msgContainer.querySelector(`.js-global-error[data-global-error-msg-compact="${msgCompact}"]`); + if (!msgDiv) { + const el = document.createElement('div'); + el.innerHTML = html`
`; + msgDiv = el.childNodes[0] as HTMLDivElement; + } + // merge duplicated messages into "the message (count)" format + const msgCount = Number(msgDiv.getAttribute(`data-global-error-msg-count`)) + 1; + msgDiv.setAttribute(`data-global-error-msg-compact`, msgCompact); + msgDiv.setAttribute(`data-global-error-msg-count`, msgCount.toString()); + msgDiv.querySelector('.ui.message')!.textContent = msg + (msgCount > 1 ? ` (${msgCount})` : ''); + msgContainer.prepend(msgDiv); +} + +export function shouldIgnoreError(err: Error) { + const ignorePatterns: Array = [ + // https://github.com/go-gitea/gitea/issues/30861 + // https://github.com/microsoft/monaco-editor/issues/4496 + // https://github.com/microsoft/monaco-editor/issues/4679 + /\/assets\/js\/.*(monaco|editor\.(api|worker))/, + ]; + for (const pattern of ignorePatterns) { + if (pattern.test(err.stack ?? '')) return true; + } + return false; +} + +export function processWindowErrorEvent({error, reason, message, type, filename, lineno, colno}: ErrorEvent & PromiseRejectionEvent) { + const err = error ?? reason; + const assetBaseUrl = String(new URL(`${window.config?.assetUrlPrefix ?? '/assets'}/`, window.location.origin)); + const {runModeIsProd} = window.config ?? {}; + + // `error` and `reason` are not guaranteed to be errors. If the value is falsy, it is likely a + // non-critical event from the browser. We log them but don't show them to users. Examples: + // - https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver#observation_errors + // - https://github.com/mozilla-mobile/firefox-ios/issues/10817 + // - https://github.com/go-gitea/gitea/issues/20240 + if (!err) { + if (message) console.error(new Error(message)); + if (runModeIsProd) return; + } + + if (err instanceof Error) { + // If the error stack trace does not include the base URL of our script assets, it likely came + // from a browser extension or inline script. Do not show such errors in production. + if (!err.stack?.includes(assetBaseUrl) && runModeIsProd) return; + // Ignore some known errors that are unable to fix + if (shouldIgnoreError(err)) return; + } + + let msg = err?.message ?? message; + if (lineno) msg += ` (${filename} @ ${lineno}:${colno})`; + const dot = msg.endsWith('.') ? '' : '.'; + const renderedType = type === 'unhandledrejection' ? 'promise rejection' : type; + showGlobalErrorMessage(`JavaScript ${renderedType}: ${msg}${dot} Open browser console to see more details.`); +} diff --git a/web_src/js/modules/message.test.ts b/web_src/js/modules/message.test.ts deleted file mode 100644 index b002ae49af19a..0000000000000 --- a/web_src/js/modules/message.test.ts +++ /dev/null @@ -1,12 +0,0 @@ -import {showGlobalErrorMessage} from './message.ts'; - -test('showGlobalErrorMessage', () => { - document.body.innerHTML = '
'; - showGlobalErrorMessage('test msg 1'); - showGlobalErrorMessage('test msg 2'); - showGlobalErrorMessage('test msg 1'); // duplicated - - expect(document.body.innerHTML).toContain('>test msg 1 (2)<'); - expect(document.body.innerHTML).toContain('>test msg 2<'); - expect(document.querySelectorAll('.js-global-error').length).toEqual(2); -}); diff --git a/web_src/js/modules/message.ts b/web_src/js/modules/message.ts deleted file mode 100644 index 1e927b0c1bc08..0000000000000 --- a/web_src/js/modules/message.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type {Intent} from '../types.ts'; -import {html} from '../utils/html.ts'; - -export function showGlobalErrorMessage(msg: string, msgType: Intent = 'error') { - const msgContainer = document.querySelector('.page-content') ?? document.body; - if (!msgContainer) { - alert(`${msgType}: ${msg}`); - return; - } - const msgCompact = msg.replace(/\W/g, '').trim(); // compact the message to a data attribute to avoid too many duplicated messages - let msgDiv = msgContainer.querySelector(`.js-global-error[data-global-error-msg-compact="${msgCompact}"]`); - if (!msgDiv) { - const el = document.createElement('div'); - el.innerHTML = html`
`; - msgDiv = el.childNodes[0] as HTMLDivElement; - } - // merge duplicated messages into "the message (count)" format - const msgCount = Number(msgDiv.getAttribute(`data-global-error-msg-count`)) + 1; - msgDiv.setAttribute(`data-global-error-msg-compact`, msgCompact); - msgDiv.setAttribute(`data-global-error-msg-count`, msgCount.toString()); - msgDiv.querySelector('.ui.message')!.textContent = msg + (msgCount > 1 ? ` (${msgCount})` : ''); - msgContainer.prepend(msgDiv); -} From deb54ff00d8f845eaab1ad35f6dc4b07b6541053 Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 27 Mar 2026 04:47:31 +0100 Subject: [PATCH 071/102] update vite --- package.json | 4 +- pnpm-lock.yaml | 176 ++++++++++++++++++++++++------------------------- 2 files changed, 90 insertions(+), 90 deletions(-) diff --git a/package.json b/package.json index 34c4ea84d4625..c8dd2856fb12f 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "tributejs": "5.1.3", "uint8-to-base64": "0.2.1", "vanilla-colorful": "0.7.2", - "vite": "8.0.2", + "vite": "8.0.3", "vite-string-plugin": "2.0.2", "vue": "3.5.31", "vue-bar-graph": "2.2.0", @@ -83,8 +83,8 @@ "eslint": "10.1.0", "eslint-import-resolver-typescript": "4.4.4", "eslint-plugin-array-func": "5.1.1", - "eslint-plugin-github": "6.0.0", "eslint-plugin-de-morgan": "2.1.1", + "eslint-plugin-github": "6.0.0", "eslint-plugin-import-x": "4.16.2", "eslint-plugin-playwright": "2.10.1", "eslint-plugin-regexp": "3.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1bc2177521ea1..56a7ab5493c2b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -64,7 +64,7 @@ importers: version: 2.1.1(tippy.js@6.3.7)(vue@3.5.31(typescript@5.9.3)) '@vitejs/plugin-vue': specifier: 6.0.5 - version: 6.0.5(vite@8.0.2(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.1))(vue@3.5.31(typescript@5.9.3)) + version: 6.0.5(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.1))(vue@3.5.31(typescript@5.9.3)) ansi_up: specifier: 6.0.6 version: 6.0.6 @@ -168,11 +168,11 @@ importers: specifier: 0.7.2 version: 0.7.2 vite: - specifier: 8.0.2 - version: 8.0.2(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.1) + specifier: 8.0.3 + version: 8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.1) vite-string-plugin: specifier: 2.0.2 - version: 2.0.2(vite@8.0.2(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.1)) + version: 2.0.2(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.1)) vue: specifier: 3.5.31 version: 3.5.31(typescript@5.9.3) @@ -239,7 +239,7 @@ importers: version: 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) '@vitest/eslint-plugin': specifier: 1.6.13 - version: 1.6.13(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3)(vitest@4.1.2(@types/node@25.5.0)(happy-dom@20.8.8)(vite@8.0.2(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.1))) + version: 1.6.13(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3)(vitest@4.1.2(@types/node@25.5.0)(happy-dom@20.8.8)(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.1))) eslint: specifier: 10.1.0 version: 10.1.0(jiti@2.6.1) @@ -332,7 +332,7 @@ importers: version: 17.12.0 vitest: specifier: 4.1.2 - version: 4.1.2(@types/node@25.5.0)(happy-dom@20.8.8)(vite@8.0.2(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.1)) + version: 4.1.2(@types/node@25.5.0)(happy-dom@20.8.8)(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.1)) vue-tsc: specifier: 3.2.6 version: 3.2.6(typescript@5.9.3) @@ -898,103 +898,103 @@ packages: resolution: {integrity: sha512-FqALmHI8D4o6lk/LRWDnhw95z5eO+eAa6ORjVg09YRR7BkcM6oPHU9uyC0gtQG5vpFLvgpeU4+zEAz2H8APHNw==} engines: {node: '>= 10'} - '@rolldown/binding-android-arm64@1.0.0-rc.11': - resolution: {integrity: sha512-SJ+/g+xNnOh6NqYxD0V3uVN4W3VfnrGsC9/hoglicgTNfABFG9JjISvkkU0dNY84MNHLWyOgxP9v9Y9pX4S7+A==} + '@rolldown/binding-android-arm64@1.0.0-rc.12': + resolution: {integrity: sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rolldown/binding-darwin-arm64@1.0.0-rc.11': - resolution: {integrity: sha512-7WQgR8SfOPwmDZGFkThUvsmd/nwAWv91oCO4I5LS7RKrssPZmOt7jONN0cW17ydGC1n/+puol1IpoieKqQidmg==} + '@rolldown/binding-darwin-arm64@1.0.0-rc.12': + resolution: {integrity: sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.0.0-rc.11': - resolution: {integrity: sha512-39Ks6UvIHq4rEogIfQBoBRusj0Q0nPVWIvqmwBLaT6aqQGIakHdESBVOPRRLacy4WwUPIx4ZKzfZ9PMW+IeyUQ==} + '@rolldown/binding-darwin-x64@1.0.0-rc.12': + resolution: {integrity: sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.0.0-rc.11': - resolution: {integrity: sha512-jfsm0ZHfhiqrvWjJAmzsqiIFPz5e7mAoCOPBNTcNgkiid/LaFKiq92+0ojH+nmJmKYkre4t71BWXUZDNp7vsag==} + '@rolldown/binding-freebsd-x64@1.0.0-rc.12': + resolution: {integrity: sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.11': - resolution: {integrity: sha512-zjQaUtSyq1nVe3nxmlSCuR96T1LPlpvmJ0SZy0WJFEsV4kFbXcq2u68L4E6O0XeFj4aex9bEauqjW8UQBeAvfQ==} + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12': + resolution: {integrity: sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.11': - resolution: {integrity: sha512-WMW1yE6IOnehTcFE9eipFkm3XN63zypWlrJQ2iF7NrQ9b2LDRjumFoOGJE8RJJTJCTBAdmLMnJ8uVitACUUo1Q==} + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12': + resolution: {integrity: sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-arm64-musl@1.0.0-rc.11': - resolution: {integrity: sha512-jfndI9tsfm4APzjNt6QdBkYwre5lRPUgHeDHoI7ydKUuJvz3lZeCfMsI56BZj+7BYqiKsJm7cfd/6KYV7ubrBg==} + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.12': + resolution: {integrity: sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.11': - resolution: {integrity: sha512-ZlFgw46NOAGMgcdvdYwAGu2Q+SLFA9LzbJLW+iyMOJyhj5wk6P3KEE9Gct4xWwSzFoPI7JCdYmYMzVtlgQ+zfw==} + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12': + resolution: {integrity: sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.11': - resolution: {integrity: sha512-hIOYmuT6ofM4K04XAZd3OzMySEO4K0/nc9+jmNcxNAxRi6c5UWpqfw3KMFV4MVFWL+jQsSh+bGw2VqmaPMTLyw==} + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12': + resolution: {integrity: sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.0.0-rc.11': - resolution: {integrity: sha512-qXBQQO9OvkjjQPLdUVr7Nr2t3QTZI7s4KZtfw7HzBgjbmAPSFwSv4rmET9lLSgq3rH/ndA3ngv3Qb8l2njoPNA==} + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.12': + resolution: {integrity: sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-musl@1.0.0-rc.11': - resolution: {integrity: sha512-/tpFfoSTzUkH9LPY+cYbqZBDyyX62w5fICq9qzsHLL8uTI6BHip3Q9Uzft0wylk/i8OOwKik8OxW+QAhDmzwmg==} + '@rolldown/binding-linux-x64-musl@1.0.0-rc.12': + resolution: {integrity: sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@rolldown/binding-openharmony-arm64@1.0.0-rc.11': - resolution: {integrity: sha512-mcp3Rio2w72IvdZG0oQ4bM2c2oumtwHfUfKncUM6zGgz0KgPz4YmDPQfnXEiY5t3+KD/i8HG2rOB/LxdmieK2g==} + '@rolldown/binding-openharmony-arm64@1.0.0-rc.12': + resolution: {integrity: sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rolldown/binding-wasm32-wasi@1.0.0-rc.11': - resolution: {integrity: sha512-LXk5Hii1Ph9asuGRjBuz8TUxdc1lWzB7nyfdoRgI0WGPZKmCxvlKk8KfYysqtr4MfGElu/f/pEQRh8fcEgkrWw==} + '@rolldown/binding-wasm32-wasi@1.0.0-rc.12': + resolution: {integrity: sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==} engines: {node: '>=14.0.0'} cpu: [wasm32] - '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.11': - resolution: {integrity: sha512-dDwf5otnx0XgRY1yqxOC4ITizcdzS/8cQ3goOWv3jFAo4F+xQYni+hnMuO6+LssHHdJW7+OCVL3CoU4ycnh35Q==} + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12': + resolution: {integrity: sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.0.0-rc.11': - resolution: {integrity: sha512-LN4/skhSggybX71ews7dAj6r2geaMJfm3kMbK2KhFMg9B10AZXnKoLCVVgzhMHL0S+aKtr4p8QbAW8k+w95bAA==} + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.12': + resolution: {integrity: sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] - '@rolldown/pluginutils@1.0.0-rc.11': - resolution: {integrity: sha512-xQO9vbwBecJRv9EUcQ/y0dzSTJgA7Q6UVN7xp6B81+tBGSLVAK03yJ9NkJaUA7JFD91kbjxRSC/mDnmvXzbHoQ==} + '@rolldown/pluginutils@1.0.0-rc.12': + resolution: {integrity: sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==} '@rolldown/pluginutils@1.0.0-rc.2': resolution: {integrity: sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw==} @@ -3481,8 +3481,8 @@ packages: robust-predicates@3.0.3: resolution: {integrity: sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==} - rolldown@1.0.0-rc.11: - resolution: {integrity: sha512-NRjoKMusSjfRbSYiH3VSumlkgFe7kYAa3pzVOsVYVFY3zb5d7nS+a3KGQ7hJKXuYWbzJKPVQ9Wxq2UvyK+ENpw==} + rolldown@1.0.0-rc.12: + resolution: {integrity: sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -3877,8 +3877,8 @@ packages: peerDependencies: vite: '*' - vite@8.0.2: - resolution: {integrity: sha512-1gFhNi+bHhRE/qKZOJXACm6tX4bA3Isy9KuKF15AgSRuRazNBOJfdDemPBU16/mpMxApDPrWvZ08DcLPEoRnuA==} + vite@8.0.3: + resolution: {integrity: sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -4550,54 +4550,54 @@ snapshots: '@resvg/resvg-wasm@2.6.2': {} - '@rolldown/binding-android-arm64@1.0.0-rc.11': + '@rolldown/binding-android-arm64@1.0.0-rc.12': optional: true - '@rolldown/binding-darwin-arm64@1.0.0-rc.11': + '@rolldown/binding-darwin-arm64@1.0.0-rc.12': optional: true - '@rolldown/binding-darwin-x64@1.0.0-rc.11': + '@rolldown/binding-darwin-x64@1.0.0-rc.12': optional: true - '@rolldown/binding-freebsd-x64@1.0.0-rc.11': + '@rolldown/binding-freebsd-x64@1.0.0-rc.12': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.11': + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12': optional: true - '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.11': + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12': optional: true - '@rolldown/binding-linux-arm64-musl@1.0.0-rc.11': + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.12': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.11': + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12': optional: true - '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.11': + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12': optional: true - '@rolldown/binding-linux-x64-gnu@1.0.0-rc.11': + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.12': optional: true - '@rolldown/binding-linux-x64-musl@1.0.0-rc.11': + '@rolldown/binding-linux-x64-musl@1.0.0-rc.12': optional: true - '@rolldown/binding-openharmony-arm64@1.0.0-rc.11': + '@rolldown/binding-openharmony-arm64@1.0.0-rc.12': optional: true - '@rolldown/binding-wasm32-wasi@1.0.0-rc.11': + '@rolldown/binding-wasm32-wasi@1.0.0-rc.12': dependencies: '@napi-rs/wasm-runtime': 1.1.1 optional: true - '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.11': + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12': optional: true - '@rolldown/binding-win32-x64-msvc@1.0.0-rc.11': + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.12': optional: true - '@rolldown/pluginutils@1.0.0-rc.11': {} + '@rolldown/pluginutils@1.0.0-rc.12': {} '@rolldown/pluginutils@1.0.0-rc.2': {} @@ -5083,13 +5083,13 @@ snapshots: d3-selection: 3.0.0 d3-transition: 3.0.1(d3-selection@3.0.0) - '@vitejs/plugin-vue@6.0.5(vite@8.0.2(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.1))(vue@3.5.31(typescript@5.9.3))': + '@vitejs/plugin-vue@6.0.5(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.1))(vue@3.5.31(typescript@5.9.3))': dependencies: '@rolldown/pluginutils': 1.0.0-rc.2 - vite: 8.0.2(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.1) + vite: 8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.1) vue: 3.5.31(typescript@5.9.3) - '@vitest/eslint-plugin@1.6.13(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3)(vitest@4.1.2(@types/node@25.5.0)(happy-dom@20.8.8)(vite@8.0.2(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.1)))': + '@vitest/eslint-plugin@1.6.13(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3)(vitest@4.1.2(@types/node@25.5.0)(happy-dom@20.8.8)(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.1)))': dependencies: '@typescript-eslint/scope-manager': 8.57.2 '@typescript-eslint/utils': 8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) @@ -5097,7 +5097,7 @@ snapshots: optionalDependencies: '@typescript-eslint/eslint-plugin': 8.57.2(@typescript-eslint/parser@8.57.2(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.1.0(jiti@2.6.1))(typescript@5.9.3) typescript: 5.9.3 - vitest: 4.1.2(@types/node@25.5.0)(happy-dom@20.8.8)(vite@8.0.2(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.1)) + vitest: 4.1.2(@types/node@25.5.0)(happy-dom@20.8.8)(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.1)) transitivePeerDependencies: - supports-color @@ -5110,13 +5110,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.2(vite@8.0.2(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.1))': + '@vitest/mocker@4.1.2(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.1))': dependencies: '@vitest/spy': 4.1.2 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.0.2(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.1) + vite: 8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.1) '@vitest/pretty-format@4.1.2': dependencies: @@ -7144,26 +7144,26 @@ snapshots: robust-predicates@3.0.3: {} - rolldown@1.0.0-rc.11: + rolldown@1.0.0-rc.12: dependencies: '@oxc-project/types': 0.122.0 - '@rolldown/pluginutils': 1.0.0-rc.11 + '@rolldown/pluginutils': 1.0.0-rc.12 optionalDependencies: - '@rolldown/binding-android-arm64': 1.0.0-rc.11 - '@rolldown/binding-darwin-arm64': 1.0.0-rc.11 - '@rolldown/binding-darwin-x64': 1.0.0-rc.11 - '@rolldown/binding-freebsd-x64': 1.0.0-rc.11 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.11 - '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.11 - '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.11 - '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.11 - '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.11 - '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.11 - '@rolldown/binding-linux-x64-musl': 1.0.0-rc.11 - '@rolldown/binding-openharmony-arm64': 1.0.0-rc.11 - '@rolldown/binding-wasm32-wasi': 1.0.0-rc.11 - '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.11 - '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.11 + '@rolldown/binding-android-arm64': 1.0.0-rc.12 + '@rolldown/binding-darwin-arm64': 1.0.0-rc.12 + '@rolldown/binding-darwin-x64': 1.0.0-rc.12 + '@rolldown/binding-freebsd-x64': 1.0.0-rc.12 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.12 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.12 + '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.12 + '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.12 + '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.12 + '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.12 + '@rolldown/binding-linux-x64-musl': 1.0.0-rc.12 + '@rolldown/binding-openharmony-arm64': 1.0.0-rc.12 + '@rolldown/binding-wasm32-wasi': 1.0.0-rc.12 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.12 + '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.12 rollup-plugin-license@3.7.0(picomatch@4.0.4)(rollup@4.60.0): dependencies: @@ -7647,16 +7647,16 @@ snapshots: vanilla-colorful@0.7.2: {} - vite-string-plugin@2.0.2(vite@8.0.2(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.1)): + vite-string-plugin@2.0.2(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.1)): dependencies: - vite: 8.0.2(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.1) + vite: 8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.1) - vite@8.0.2(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.1): + vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.1): dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 postcss: 8.5.8 - rolldown: 1.0.0-rc.11 + rolldown: 1.0.0-rc.12 tinyglobby: 0.2.15 optionalDependencies: '@types/node': 25.5.0 @@ -7665,10 +7665,10 @@ snapshots: jiti: 2.6.1 terser: 5.46.1 - vitest@4.1.2(@types/node@25.5.0)(happy-dom@20.8.8)(vite@8.0.2(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.1)): + vitest@4.1.2(@types/node@25.5.0)(happy-dom@20.8.8)(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.1)): dependencies: '@vitest/expect': 4.1.2 - '@vitest/mocker': 4.1.2(vite@8.0.2(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.1)) + '@vitest/mocker': 4.1.2(vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.1)) '@vitest/pretty-format': 4.1.2 '@vitest/runner': 4.1.2 '@vitest/snapshot': 4.1.2 @@ -7685,7 +7685,7 @@ snapshots: tinyexec: 1.0.4 tinyglobby: 0.2.15 tinyrainbow: 3.1.0 - vite: 8.0.2(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.1) + vite: 8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.46.1) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 25.5.0 From 77921c8adc634406ee1b2eb54f8b5d6f9b832842 Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 27 Mar 2026 08:19:34 +0100 Subject: [PATCH 072/102] Add Vite dev server mode with reverse proxy - Add Go reverse proxy (modules/public/vitedev.go) that forwards Vite requests (/@vite/, /@fs/, /@id/, /__vite, /node_modules/, /web_src/, ?import, ?raw) from the backend port to the Vite dev server - Add http.Hijacker support to response writers for WebSocket upgrades (vite-hmr and vite-ping protocols) through the proxy chain - Add AssetURL template function that returns dev source paths or prod manifest-hashed paths depending on Vite dev server availability - Add in-memory IIFE build in dev mode via iifePlugin configureServer, with file watcher for automatic rebuild and full-reload on changes - Add port file discovery (public/assets/.vite/dev-port) for lazy proxy initialization via atomic.Pointer - Update watch-frontend to run `vite build` then `vite dev` - Set appType: 'custom' to disable Vite HTML handling - Add server.warmup for faster initial page loads - Add Cache-Control: no-store header to prevent browser disk cache - Move chunkSizeWarningLimit and assetsInlineLimit to commonViteOpts Co-Authored-By: Claude (claude-opus-4-6) --- Makefile | 6 +- modules/public/vitedev.go | 133 +++++++++++++++++++++++++++++++ modules/templates/helper.go | 1 + modules/web/handler.go | 9 +++ routers/web/web.go | 4 + services/context/response.go | 11 +++ templates/base/footer.tmpl | 2 +- templates/base/head_script.tmpl | 2 +- templates/base/head_style.tmpl | 4 +- vite.config.ts | 137 +++++++++++++++++++++++++------- 10 files changed, 274 insertions(+), 35 deletions(-) create mode 100644 modules/public/vitedev.go diff --git a/Makefile b/Makefile index b8464c60a55a5..805cfd3b0f0b8 100644 --- a/Makefile +++ b/Makefile @@ -380,9 +380,9 @@ watch: ## watch everything and continuously rebuild @bash tools/watch.sh .PHONY: watch-frontend -watch-frontend: node_modules ## watch frontend files and continuously rebuild - @rm -rf $(FRONTEND_DEST_ENTRIES) - NODE_ENV=development $(NODE_VARS) pnpm exec vite build --watch +watch-frontend: node_modules ## start vite dev server for frontend + @NODE_ENV=development $(NODE_VARS) pnpm exec vite build + NODE_ENV=development $(NODE_VARS) pnpm exec vite dev .PHONY: watch-backend watch-backend: ## watch backend files and continuously rebuild diff --git a/modules/public/vitedev.go b/modules/public/vitedev.go new file mode 100644 index 0000000000000..9b68d6dde9888 --- /dev/null +++ b/modules/public/vitedev.go @@ -0,0 +1,133 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package public + +import ( + "net/http" + "net/http/httputil" + "net/url" + "os" + "path/filepath" + "strings" + "sync/atomic" + + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" +) + +const viteDevPortFile = "public/assets/.vite/dev-port" + +var viteDevProxy atomic.Pointer[httputil.ReverseProxy] + +func getViteDevProxy() *httputil.ReverseProxy { + if proxy := viteDevProxy.Load(); proxy != nil { + return proxy + } + + portFile := filepath.Join(setting.StaticRootPath, viteDevPortFile) + data, err := os.ReadFile(portFile) + if err != nil { + return nil + } + port := strings.TrimSpace(string(data)) + if port == "" { + return nil + } + + target, err := url.Parse("http://localhost:" + port) + if err != nil { + log.Error("Failed to parse Vite dev server URL: %v", err) + return nil + } + + log.Info("Proxying Vite dev server requests to %s", target) + proxy := &httputil.ReverseProxy{ + Rewrite: func(r *httputil.ProxyRequest) { + r.SetURL(target) + r.Out.Host = target.Host + }, + } + viteDevProxy.Store(proxy) + return proxy +} + +// ViteDevMiddleware proxies matching requests to the Vite dev server. +// It is registered as middleware in non-production mode and lazily discovers +// the Vite dev server port from the port file written by the viteDevServerPortPlugin. +func ViteDevMiddleware(resp http.ResponseWriter, req *http.Request) { + if !IsViteDevRequest(req) { + return + } + proxy := getViteDevProxy() + if proxy == nil { + return + } + proxy.ServeHTTP(resp, req) +} + +// IsViteDevMode returns true if the Vite dev server port file exists. +func IsViteDevMode() bool { + portFile := filepath.Join(setting.StaticRootPath, viteDevPortFile) + _, err := os.Stat(portFile) + return err == nil +} + +// AssetURL returns the full URL path for a frontend asset. +// 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 AssetURL(name string) string { + if IsViteDevMode() { + if src := viteDevSourceURL(name); src != "" { + return src + } + } + return setting.StaticURLPrefix + "/assets/" + GetAssetPath(name) +} + +func viteDevSourceURL(name string) string { + if strings.HasPrefix(name, "css/theme-") { + return setting.AppSubURL + "/web_src/css/themes/" + strings.TrimPrefix(name, "css/") + } + if strings.HasPrefix(name, "css/") { + return setting.AppSubURL + "/web_src/" + name + } + if name == "js/iife.js" { + return setting.AppSubURL + "/__vite_iife.js" + } + if name == "js/index.js" { + return setting.AppSubURL + "/web_src/js/index.ts" + } + return "" +} + +// IsViteDevRequest returns true if the request should be proxied to the Vite dev server. +// Vite internal prefixes are defined in the Vite source: +// - packages/vite/src/node/constants.ts (/@vite/, /@fs/, /__vite) +// - packages/vite/src/shared/constants.ts (/@id/) +// - packages/vite/src/node/server/ws.ts (vite-hmr, vite-ping WebSocket protocols) +// - packages/vite/src/node/utils.ts (?import, ?raw query params) +func IsViteDevRequest(req *http.Request) bool { + wsProtocol := req.Header.Get("Sec-WebSocket-Protocol") + if req.Header.Get("Upgrade") == "websocket" && (wsProtocol == "vite-hmr" || wsProtocol == "vite-ping") { + return true + } + path := req.URL.Path + if strings.HasPrefix(path, "/@vite/") || + strings.HasPrefix(path, "/@fs/") || + strings.HasPrefix(path, "/@id/") || + strings.HasPrefix(path, "/__vite") || + strings.HasPrefix(path, "/node_modules/") || + strings.HasPrefix(path, "/web_src/") { + return true + } + query := req.URL.Query() + if _, ok := query["import"]; ok { + return true + } + if _, ok := query["raw"]; ok { + return true + } + return false +} diff --git a/modules/templates/helper.go b/modules/templates/helper.go index c69624075c568..6a99d76c160a6 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -94,6 +94,7 @@ func NewFuncMap() template.FuncMap { return setting.Domain }, "GetAssetPath": public.GetAssetPath, + "AssetURL": public.AssetURL, "ShowFooterTemplateLoadTime": func() bool { return setting.Other.ShowFooterTemplateLoadTime }, diff --git a/modules/web/handler.go b/modules/web/handler.go index d113bbba451f5..46865d0f40519 100644 --- a/modules/web/handler.go +++ b/modules/web/handler.go @@ -4,7 +4,9 @@ package web import ( + "bufio" "fmt" + "net" "net/http" "reflect" @@ -47,6 +49,13 @@ func (r *responseWriter) WriteHeader(statusCode int) { r.respWriter.WriteHeader(statusCode) } +func (r *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { + if hj, ok := r.respWriter.(http.Hijacker); ok { + return hj.Hijack() + } + return nil, nil, http.ErrNotSupported +} + var ( httpReqType = reflect.TypeFor[*http.Request]() respWriterType = reflect.TypeFor[http.ResponseWriter]() diff --git a/routers/web/web.go b/routers/web/web.go index 75cc437b43ef7..f2f88fc23144d 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -259,6 +259,10 @@ func Routes() *web.Router { // GetHead allows a HEAD request redirect to GET if HEAD method is not defined for that route routes.BeforeRouting(chi_middleware.GetHead) + if !setting.IsProd { + routes.BeforeRouting(public.ViteDevMiddleware) + } + routes.Head("/", misc.DummyOK) // for health check - doesn't need to be passed through gzip handler routes.Methods("GET, HEAD, OPTIONS", "/assets/*", optionsCorsHandler(), public.FileHandlerFunc()) routes.Methods("GET, HEAD", "/avatars/*", avatarStorageHandler(setting.Avatar.Storage, "avatars", storage.Avatars)) diff --git a/services/context/response.go b/services/context/response.go index c7368ebc6f0a8..10a51e04c59d5 100644 --- a/services/context/response.go +++ b/services/context/response.go @@ -4,6 +4,8 @@ package context import ( + "bufio" + "net" "net/http" web_types "code.gitea.io/gitea/modules/web/types" @@ -67,6 +69,15 @@ func (r *Response) WriteHeader(statusCode int) { } } +// Hijack implements http.Hijacker, delegating to the underlying ResponseWriter. +// This is needed for WebSocket upgrades through reverse proxies. +func (r *Response) Hijack() (net.Conn, *bufio.ReadWriter, error) { + if hj, ok := r.ResponseWriter.(http.Hijacker); ok { + return hj.Hijack() + } + return nil, nil, http.ErrNotSupported +} + // Flush flushes cached data func (r *Response) Flush() { if f, ok := r.ResponseWriter.(http.Flusher); ok { diff --git a/templates/base/footer.tmpl b/templates/base/footer.tmpl index b775212172dd8..0ebeef7107c6f 100644 --- a/templates/base/footer.tmpl +++ b/templates/base/footer.tmpl @@ -9,7 +9,7 @@ {{template "custom/body_outer_post" .}} {{template "base/footer_content" .}} - + {{template "custom/footer" .}} diff --git a/templates/base/head_script.tmpl b/templates/base/head_script.tmpl index 7585c278a5729..2a603e3932c5f 100644 --- a/templates/base/head_script.tmpl +++ b/templates/base/head_script.tmpl @@ -31,4 +31,4 @@ If you introduce mistakes in it, Gitea JavaScript code wouldn't run correctly. {{/* in case some pages don't render the pageData, we make sure it is an object to prevent null access */}} window.config.pageData = window.config.pageData || {}; - + diff --git a/templates/base/head_style.tmpl b/templates/base/head_style.tmpl index 75aaf77dda0b7..480dc49359fa1 100644 --- a/templates/base/head_style.tmpl +++ b/templates/base/head_style.tmpl @@ -1,2 +1,2 @@ - - + + diff --git a/vite.config.ts b/vite.config.ts index 738feb1e231f0..d09e2d1bac7bd 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -54,6 +54,8 @@ function commonViteOpts({build, ...other}: InlineConfig): InlineConfig { target: 'es2020', minify: isProduction ? 'oxc' : false, cssMinify: isProduction ? 'esbuild' : false, + chunkSizeWarningLimit: Infinity, + assetsInlineLimit: 32768, reportCompressedSize: false, rolldownOptions: { ...commonRolldownOptions, @@ -65,41 +67,82 @@ function commonViteOpts({build, ...other}: InlineConfig): InlineConfig { }; } -// Build iife.js as a blocking IIFE bundle to avoid pop-in effects +const iifeEntry = join(import.meta.dirname, 'web_src/js/iife.ts'); + +function iifeBuildOpts({entryFileNames, write}: {entryFileNames: string, write?: boolean}) { + return commonViteOpts({ + build: { + lib: {entry: iifeEntry, formats: ['iife'], name: 'iife'}, + rolldownOptions: {output: {entryFileNames}}, + ...(write === false && {write: false}), + }, + plugins: [stringPlugin()], + }); +} + +// Build iife.js as a blocking IIFE bundle. In dev mode, serves it from memory +// and rebuilds on file changes. In prod mode, writes to disk during closeBundle. function iifePlugin(): Plugin { + let iifeCode = ''; + let iifeMap = ''; + const iifeModules = new Set(); + let isBuilding = false; return { name: 'iife', - async closeBundle() { - // Clean up old hashed files before rebuilding - for (const file of globSync('js/iife.*.js*', {cwd: outDir})) unlinkSync(join(outDir, file)); + async configureServer(server) { + const buildAndCache = async () => { + const result = await build(iifeBuildOpts({entryFileNames: 'js/iife.js', write: false})); + const output = (Array.isArray(result) ? result[0] : result) as Rolldown.RolldownOutput; + const chunk = output.output[0]; + iifeCode = chunk.code.replace(/\/\/# sourceMappingURL=.*/, '//# sourceMappingURL=__vite_iife.js.map'); + const mapAsset = output.output.find((o) => o.fileName.endsWith('.map')); + iifeMap = mapAsset && 'source' in mapAsset ? String(mapAsset.source) : ''; + iifeModules.clear(); + for (const id of Object.keys(chunk.modules)) iifeModules.add(id); + }; + await buildAndCache(); - const result = await build(commonViteOpts({ - build: { - lib: { - entry: join(import.meta.dirname, 'web_src/js/iife.ts'), - formats: ['iife'], - name: 'iife', - }, - rolldownOptions: { - output: { - entryFileNames: 'js/iife.[hash:8].js', - }, - }, - }, - define: { - // needed for tippy.js - 'process.env.NODE_ENV': JSON.stringify(isProduction ? 'production' : 'development'), - }, - plugins: [ - stringPlugin(), - ], - })); + let needsRebuild = false; + server.watcher.on('change', async (path) => { + if (!iifeModules.has(path)) return; + needsRebuild = true; + if (isBuilding) return; + isBuilding = true; + try { + do { + needsRebuild = false; + await buildAndCache(); + } while (needsRebuild); + server.ws.send({type: 'full-reload'}); + } finally { + isBuilding = false; + } + }); - // Append IIFE entry to the main Vite manifest - const manifestPath = join(outDir, '.vite', 'manifest.json'); + server.middlewares.use((req, res, next) => { + const pathname = req.url!.split('?')[0]; + if (pathname === '/__vite_iife.js') { + res.setHeader('Content-Type', 'application/javascript'); + res.setHeader('Cache-Control', 'no-store'); + res.end(iifeCode); + return; + } + if (pathname === '/__vite_iife.js.map') { + res.setHeader('Content-Type', 'application/json'); + res.setHeader('Cache-Control', 'no-store'); + res.end(iifeMap); + return; + } + next(); + }); + }, + async closeBundle() { + for (const file of globSync('js/iife.*.js*', {cwd: outDir})) unlinkSync(join(outDir, file)); + const result = await build(iifeBuildOpts({entryFileNames: 'js/iife.[hash:8].js'})); const buildOutput = (Array.isArray(result) ? result[0] : result) as Rolldown.RolldownOutput; const entry = buildOutput.output.find((o) => o.fileName.startsWith('js/iife.')); if (!entry) throw new Error('IIFE build produced no output'); + const manifestPath = join(outDir, '.vite', 'manifest.json'); writeFileSync(manifestPath, JSON.stringify({ ...JSON.parse(readFileSync(manifestPath, 'utf8')), 'web_src/js/iife.ts': {file: entry.fileName, name: 'iife', isEntry: true}, @@ -120,11 +163,48 @@ function filterCssUrlPlugin(): Plugin { }; } +const viteDevServerPort = Number(env.VITE_DEV_SERVER_PORT) || 3001; +const viteDevPortFilePath = join(outDir, '.vite', 'dev-port'); + +// Write the Vite dev server's actual port to a file so the Go server can discover it for proxying. +function viteDevServerPortPlugin(): Plugin { + return { + name: 'vite-dev-server-port', + apply: 'serve', + configureServer(server) { + server.httpServer!.once('listening', () => { + const addr = server.httpServer!.address(); + if (typeof addr === 'object' && addr) { + writeFileSync(viteDevPortFilePath, String(addr.port)); + } + }); + }, + }; +} + export default defineConfig(commonViteOpts({ + appType: 'custom', // Go serves all HTML, disable Vite's HTML handling + clearScreen: false, + server: { + port: viteDevServerPort, + open: false, + host: '0.0.0.0', + strictPort: false, + headers: { + 'Cache-Control': 'no-store', // prevent browser disk cache + }, + warmup: { + clientFiles: [ + // warmup the important entry points + 'web_src/js/index.ts', + 'web_src/css/index.css', + 'web_src/css/themes/*.css', + ], + }, + }, build: { modulePreload: false, manifest: true, - chunkSizeWarningLimit: Infinity, rolldownOptions: { input: { index: join(import.meta.dirname, 'web_src/js/index.ts'), @@ -171,6 +251,7 @@ export default defineConfig(commonViteOpts({ }, plugins: [ iifePlugin(), + viteDevServerPortPlugin(), filterCssUrlPlugin(), stringPlugin(), vuePlugin({ From a6f4c7b38ed78438102fc9c215c008723ad0bae6 Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 27 Mar 2026 08:29:44 +0100 Subject: [PATCH 073/102] Migrate all asset URLs to AssetURL, remove pre-build step - Replace all `{{AssetUrlPrefix}}/{{GetAssetPath ...}}` in templates and `setting.StaticURLPrefix + "/assets/" + public.GetAssetPath(...)` in Go code with single `AssetURL(...)` calls - Remove `GetAssetPath` from template function map - Serve sharedworker through Vite dev proxy, add mapping in `viteDevSourceURL` for `js/sharedworker.js` - Use `sharedWorkerPath` as full URL in JS (remove `assetUrlPrefix` prefix construction in notification.ts and stopwatch.ts) - Remove `vite build` pre-build step from `watch-frontend` target, all assets now served through Vite dev server - Skip `IsViteDevMode()` filesystem check in production mode Co-Authored-By: Claude (claude-opus-4-6) --- Makefile | 1 - modules/markup/external/openapi.go | 10 ++++------ modules/markup/render.go | 4 ++-- modules/public/vitedev.go | 7 +++++++ modules/templates/helper.go | 3 +-- templates/base/head_script.tmpl | 2 +- templates/devtest/devtest-footer.tmpl | 2 +- templates/devtest/devtest-header.tmpl | 2 +- templates/status/500.tmpl | 2 +- templates/swagger/ui.tmpl | 4 ++-- tests/integration/markup_external_test.go | 4 ++-- web_src/js/features/notification.ts | 4 ++-- web_src/js/features/stopwatch.ts | 4 ++-- 13 files changed, 26 insertions(+), 23 deletions(-) diff --git a/Makefile b/Makefile index 805cfd3b0f0b8..18553ddd5b040 100644 --- a/Makefile +++ b/Makefile @@ -381,7 +381,6 @@ watch: ## watch everything and continuously rebuild .PHONY: watch-frontend watch-frontend: node_modules ## start vite dev server for frontend - @NODE_ENV=development $(NODE_VARS) pnpm exec vite build NODE_ENV=development $(NODE_VARS) pnpm exec vite dev .PHONY: watch-backend diff --git a/modules/markup/external/openapi.go b/modules/markup/external/openapi.go index e30382704dd4d..bea410660640f 100644 --- a/modules/markup/external/openapi.go +++ b/modules/markup/external/openapi.go @@ -62,19 +62,17 @@ func (p *openAPIRenderer) Render(ctx *markup.RenderContext, input io.Reader, out - +
- + `, - setting.StaticURLPrefix, - public.GetAssetPath("css/swagger.css"), + public.AssetURL("css/swagger.css"), html.EscapeString(ctx.RenderOptions.RelativePath), html.EscapeString(util.UnsafeBytesToString(content)), - setting.StaticURLPrefix, - public.GetAssetPath("js/swagger.js"), + public.AssetURL("js/swagger.js"), )) return err } diff --git a/modules/markup/render.go b/modules/markup/render.go index fb7879bfc9394..419a86d4e4242 100644 --- a/modules/markup/render.go +++ b/modules/markup/render.go @@ -238,8 +238,8 @@ 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/" + public.GetAssetPath("css/external-render-iframe.css") - extraScriptSrc := setting.AppSubURL + "/assets/" + public.GetAssetPath("js/external-render-iframe.js") + extraStyleHref := public.AssetURL("css/external-render-iframe.css") + extraScriptSrc := public.AssetURL("js/external-render-iframe.js") // "`, extraScriptSrc, extraStyleHref) } diff --git a/modules/public/vitedev.go b/modules/public/vitedev.go index 9b68d6dde9888..47994ed588f66 100644 --- a/modules/public/vitedev.go +++ b/modules/public/vitedev.go @@ -67,7 +67,11 @@ func ViteDevMiddleware(resp http.ResponseWriter, req *http.Request) { } // IsViteDevMode returns true if the Vite dev server port file exists. +// In production mode, the result is cached after the first check. func IsViteDevMode() bool { + if setting.IsProd { + return false + } portFile := filepath.Join(setting.StaticRootPath, viteDevPortFile) _, err := os.Stat(portFile) return err == nil @@ -93,6 +97,9 @@ func viteDevSourceURL(name string) string { if strings.HasPrefix(name, "css/") { return setting.AppSubURL + "/web_src/" + name } + if name == "js/sharedworker.js" { + return setting.AppSubURL + "/web_src/js/features/sharedworker.ts" + } if name == "js/iife.js" { return setting.AppSubURL + "/__vite_iife.js" } diff --git a/modules/templates/helper.go b/modules/templates/helper.go index 6a99d76c160a6..459ed5b0baa3f 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -93,8 +93,7 @@ func NewFuncMap() template.FuncMap { "AppDomain": func() string { // documented in mail-templates.md return setting.Domain }, - "GetAssetPath": public.GetAssetPath, - "AssetURL": public.AssetURL, + "AssetURL": public.AssetURL, "ShowFooterTemplateLoadTime": func() bool { return setting.Other.ShowFooterTemplateLoadTime }, diff --git a/templates/base/head_script.tmpl b/templates/base/head_script.tmpl index 2a603e3932c5f..fd808e9ec1783 100644 --- a/templates/base/head_script.tmpl +++ b/templates/base/head_script.tmpl @@ -16,7 +16,7 @@ If you introduce mistakes in it, Gitea JavaScript code wouldn't run correctly. notificationSettings: {{NotificationSettings}}, {{/*a map provided by NewFuncMap in helper.go*/}} enableTimeTracking: {{EnableTimetracking}}, mermaidMaxSourceCharacters: {{MermaidMaxSourceCharacters}}, - sharedWorkerPath: '{{GetAssetPath "js/sharedworker.js"}}', + sharedWorkerPath: '{{AssetURL "js/sharedworker.js"}}', {{/* this global i18n object should only contain general texts. for specialized texts, it should be provided inside the related modules by: (1) API response (2) HTML data-attribute (3) PageData */}} i18n: { copy_success: {{ctx.Locale.Tr "copy_success"}}, diff --git a/templates/devtest/devtest-footer.tmpl b/templates/devtest/devtest-footer.tmpl index 936874ab7aaac..1936e441db890 100644 --- a/templates/devtest/devtest-footer.tmpl +++ b/templates/devtest/devtest-footer.tmpl @@ -1,3 +1,3 @@ {{/* TODO: the devtest.js is isolated from index.js, so no module is shared and many index.js functions do not work in devtest.ts */}} - + {{template "base/footer" ctx.RootData}} diff --git a/templates/devtest/devtest-header.tmpl b/templates/devtest/devtest-header.tmpl index 3f5f6e5ab7492..6e3929081e50c 100644 --- a/templates/devtest/devtest-header.tmpl +++ b/templates/devtest/devtest-header.tmpl @@ -1,5 +1,5 @@ {{template "base/head" ctx.RootData}} - + + diff --git a/tests/integration/markup_external_test.go b/tests/integration/markup_external_test.go index 0e9919d679b9c..4ac15f311094a 100644 --- a/tests/integration/markup_external_test.go +++ b/tests/integration/markup_external_test.go @@ -108,7 +108,7 @@ func TestExternalMarkupRenderer(t *testing.T) { // default sandbox in sub page response assert.Equal(t, "frame-src 'self'; sandbox allow-scripts allow-popups", respSub.Header().Get("Content-Security-Policy")) // FIXME: actually here is a bug (legacy design problem), the "PostProcess" will escape "
<script></script>
`, respSub.Body.String()) + assert.Equal(t, `
<script></script>
`, respSub.Body.String()) }) }) @@ -131,7 +131,7 @@ func TestExternalMarkupRenderer(t *testing.T) { t.Run("HTMLContentWithExternalRenderIframeHelper", func(t *testing.T) { req := NewRequest(t, "GET", "/user2/repo1/render/branch/master/html.no-sanitizer") respSub := MakeRequest(t, req, http.StatusOK) - assert.Equal(t, ``, respSub.Body.String()) + assert.Equal(t, ``, respSub.Body.String()) assert.Equal(t, "frame-src 'self'", respSub.Header().Get("Content-Security-Policy")) }) }) diff --git a/web_src/js/features/notification.ts b/web_src/js/features/notification.ts index 83aa59ec961c3..a7cfdab2c6489 100644 --- a/web_src/js/features/notification.ts +++ b/web_src/js/features/notification.ts @@ -2,7 +2,7 @@ import {GET} from '../modules/fetch.ts'; import {toggleElem, createElementFromHTML} from '../utils/dom.ts'; import {logoutFromWorker} from '../modules/worker.ts'; -const {appSubUrl, notificationSettings, assetUrlPrefix, sharedWorkerPath} = window.config; +const {appSubUrl, notificationSettings, sharedWorkerPath} = window.config; let notificationSequenceNumber = 0; async function receiveUpdateCount(event: MessageEvent<{type: string, data: string}>) { @@ -33,7 +33,7 @@ export function initNotificationCount() { if (notificationSettings.EventSourceUpdateTime > 0 && window.EventSource && window.SharedWorker) { // Try to connect to the event source via the shared worker first - const worker = new SharedWorker(`${assetUrlPrefix}/${sharedWorkerPath}`, 'notification-worker'); + const worker = new SharedWorker(sharedWorkerPath, 'notification-worker'); worker.addEventListener('error', (event) => { console.error('worker error', event); }); diff --git a/web_src/js/features/stopwatch.ts b/web_src/js/features/stopwatch.ts index 306f50a564de7..179808574211a 100644 --- a/web_src/js/features/stopwatch.ts +++ b/web_src/js/features/stopwatch.ts @@ -3,7 +3,7 @@ import {GET} from '../modules/fetch.ts'; import {hideElem, queryElems, showElem} from '../utils/dom.ts'; import {logoutFromWorker} from '../modules/worker.ts'; -const {appSubUrl, notificationSettings, enableTimeTracking, assetUrlPrefix, sharedWorkerPath} = window.config; +const {appSubUrl, notificationSettings, enableTimeTracking, sharedWorkerPath} = window.config; export function initStopwatch() { if (!enableTimeTracking) { @@ -47,7 +47,7 @@ export function initStopwatch() { // if the browser supports EventSource and SharedWorker, use it instead of the periodic poller if (notificationSettings.EventSourceUpdateTime > 0 && window.EventSource && window.SharedWorker) { // Try to connect to the event source via the shared worker first - const worker = new SharedWorker(`${assetUrlPrefix}/${sharedWorkerPath}`, 'notification-worker'); + const worker = new SharedWorker(sharedWorkerPath, 'notification-worker'); worker.addEventListener('error', (event) => { console.error('worker error', event); }); From 9b0c5f250c3d05dc2728d59842f5ac426c1b7a40 Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 27 Mar 2026 08:51:00 +0100 Subject: [PATCH 074/102] Rename AssetURL to AssetPath, unexport GetAssetPath - Rename `AssetURL` to `AssetPath` (returns a path, not a URL) - Unexport `GetAssetPath` to `getAssetPath` (only used within `modules/public`) Co-Authored-By: Claude (claude-opus-4-6) --- modules/markup/external/openapi.go | 4 ++-- modules/markup/render.go | 4 ++-- modules/public/manifest.go | 8 ++++---- modules/public/manifest_test.go | 8 ++++---- modules/public/vitedev.go | 6 +++--- modules/templates/helper.go | 2 +- templates/base/footer.tmpl | 2 +- templates/base/head_script.tmpl | 4 ++-- templates/base/head_style.tmpl | 4 ++-- templates/devtest/devtest-footer.tmpl | 2 +- templates/devtest/devtest-header.tmpl | 2 +- templates/status/500.tmpl | 2 +- templates/swagger/ui.tmpl | 4 ++-- tests/integration/markup_external_test.go | 4 ++-- 14 files changed, 28 insertions(+), 28 deletions(-) diff --git a/modules/markup/external/openapi.go b/modules/markup/external/openapi.go index bea410660640f..e74f53250b2b9 100644 --- a/modules/markup/external/openapi.go +++ b/modules/markup/external/openapi.go @@ -69,10 +69,10 @@ func (p *openAPIRenderer) Render(ctx *markup.RenderContext, input io.Reader, out `, - public.AssetURL("css/swagger.css"), + public.AssetPath("css/swagger.css"), html.EscapeString(ctx.RenderOptions.RelativePath), html.EscapeString(util.UnsafeBytesToString(content)), - public.AssetURL("js/swagger.js"), + public.AssetPath("js/swagger.js"), )) return err } diff --git a/modules/markup/render.go b/modules/markup/render.go index 419a86d4e4242..e7dc1120133ba 100644 --- a/modules/markup/render.go +++ b/modules/markup/render.go @@ -238,8 +238,8 @@ 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 := public.AssetURL("css/external-render-iframe.css") - extraScriptSrc := public.AssetURL("js/external-render-iframe.js") + extraStyleHref := public.AssetPath("css/external-render-iframe.css") + extraScriptSrc := public.AssetPath("js/external-render-iframe.js") // "`, extraScriptSrc, extraStyleHref) } diff --git a/modules/public/manifest.go b/modules/public/manifest.go index 2b8c0681d1efb..ef01bc62ce765 100644 --- a/modules/public/manifest.go +++ b/modules/public/manifest.go @@ -65,7 +65,7 @@ func reloadManifest() *manifestDataStruct { now := time.Now() data := manifestData.Load() if data != nil && now.Sub(data.checkTime) < time.Second { - // a single request triggers multiple calls to GetAssetPath + // a single request triggers multiple calls to getAssetPath // do not check the manifest file too frequently return data } @@ -116,10 +116,10 @@ func getManifestPaths() map[string]string { return nil } -// GetAssetPath resolves an unhashed asset path to its content-hashed path from the frontend manifest. -// Example: GetAssetPath("js/index.js") returns "js/index.C6Z2MRVQ.js" +// getAssetPath resolves an unhashed asset path to its content-hashed path from the frontend 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 { +func getAssetPath(name string) string { paths := getManifestPaths() if p, ok := paths[name]; ok { return p diff --git a/modules/public/manifest_test.go b/modules/public/manifest_test.go index 50f7e0df7e093..f17a596e03775 100644 --- a/modules/public/manifest_test.go +++ b/modules/public/manifest_test.go @@ -61,12 +61,12 @@ func TestParseManifest(t *testing.T) { assert.Empty(t, paths["js/chunk.js"]) } -func TestGetAssetPathFallback(t *testing.T) { - // When manifest is not loaded, GetAssetPath should return the input as-is +func TestgetAssetPathFallback(t *testing.T) { + // When manifest is not loaded, getAssetPath should return the input as-is old := manifestData.Load() manifestData.Store(&manifestDataStruct{paths: make(map[string]string)}) defer func() { manifestData.Store(old) }() - assert.Equal(t, "js/index.js", GetAssetPath("js/index.js")) - assert.Equal(t, "css/theme-gitea-dark.css", GetAssetPath("css/theme-gitea-dark.css")) + assert.Equal(t, "js/index.js", getAssetPath("js/index.js")) + assert.Equal(t, "css/theme-gitea-dark.css", getAssetPath("css/theme-gitea-dark.css")) } diff --git a/modules/public/vitedev.go b/modules/public/vitedev.go index 47994ed588f66..ad3eb2cd36440 100644 --- a/modules/public/vitedev.go +++ b/modules/public/vitedev.go @@ -77,17 +77,17 @@ func IsViteDevMode() bool { return err == nil } -// AssetURL returns the full URL path for a frontend asset. +// AssetPath returns the rooted path for a frontend asset. // 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 AssetURL(name string) string { +func AssetPath(name string) string { if IsViteDevMode() { if src := viteDevSourceURL(name); src != "" { return src } } - return setting.StaticURLPrefix + "/assets/" + GetAssetPath(name) + return setting.StaticURLPrefix + "/assets/" + getAssetPath(name) } func viteDevSourceURL(name string) string { diff --git a/modules/templates/helper.go b/modules/templates/helper.go index 459ed5b0baa3f..0936093d48528 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -93,7 +93,7 @@ func NewFuncMap() template.FuncMap { "AppDomain": func() string { // documented in mail-templates.md return setting.Domain }, - "AssetURL": public.AssetURL, + "AssetPath": public.AssetPath, "ShowFooterTemplateLoadTime": func() bool { return setting.Other.ShowFooterTemplateLoadTime }, diff --git a/templates/base/footer.tmpl b/templates/base/footer.tmpl index 0ebeef7107c6f..63b630cfa5659 100644 --- a/templates/base/footer.tmpl +++ b/templates/base/footer.tmpl @@ -9,7 +9,7 @@ {{template "custom/body_outer_post" .}} {{template "base/footer_content" .}} - + {{template "custom/footer" .}} diff --git a/templates/base/head_script.tmpl b/templates/base/head_script.tmpl index fd808e9ec1783..8f912fc897d85 100644 --- a/templates/base/head_script.tmpl +++ b/templates/base/head_script.tmpl @@ -16,7 +16,7 @@ If you introduce mistakes in it, Gitea JavaScript code wouldn't run correctly. notificationSettings: {{NotificationSettings}}, {{/*a map provided by NewFuncMap in helper.go*/}} enableTimeTracking: {{EnableTimetracking}}, mermaidMaxSourceCharacters: {{MermaidMaxSourceCharacters}}, - sharedWorkerPath: '{{AssetURL "js/sharedworker.js"}}', + sharedWorkerPath: '{{AssetPath "js/sharedworker.js"}}', {{/* this global i18n object should only contain general texts. for specialized texts, it should be provided inside the related modules by: (1) API response (2) HTML data-attribute (3) PageData */}} i18n: { copy_success: {{ctx.Locale.Tr "copy_success"}}, @@ -31,4 +31,4 @@ If you introduce mistakes in it, Gitea JavaScript code wouldn't run correctly. {{/* in case some pages don't render the pageData, we make sure it is an object to prevent null access */}} window.config.pageData = window.config.pageData || {}; - + diff --git a/templates/base/head_style.tmpl b/templates/base/head_style.tmpl index 480dc49359fa1..6b1a9dee282e7 100644 --- a/templates/base/head_style.tmpl +++ b/templates/base/head_style.tmpl @@ -1,2 +1,2 @@ - - + + diff --git a/templates/devtest/devtest-footer.tmpl b/templates/devtest/devtest-footer.tmpl index 1936e441db890..46c50428938ba 100644 --- a/templates/devtest/devtest-footer.tmpl +++ b/templates/devtest/devtest-footer.tmpl @@ -1,3 +1,3 @@ {{/* TODO: the devtest.js is isolated from index.js, so no module is shared and many index.js functions do not work in devtest.ts */}} - + {{template "base/footer" ctx.RootData}} diff --git a/templates/devtest/devtest-header.tmpl b/templates/devtest/devtest-header.tmpl index 6e3929081e50c..5653481ff3938 100644 --- a/templates/devtest/devtest-header.tmpl +++ b/templates/devtest/devtest-header.tmpl @@ -1,5 +1,5 @@ {{template "base/head" ctx.RootData}} - + + diff --git a/tests/integration/markup_external_test.go b/tests/integration/markup_external_test.go index 4ac15f311094a..e12b44bc5171e 100644 --- a/tests/integration/markup_external_test.go +++ b/tests/integration/markup_external_test.go @@ -108,7 +108,7 @@ func TestExternalMarkupRenderer(t *testing.T) { // default sandbox in sub page response assert.Equal(t, "frame-src 'self'; sandbox allow-scripts allow-popups", respSub.Header().Get("Content-Security-Policy")) // FIXME: actually here is a bug (legacy design problem), the "PostProcess" will escape "
<script></script>
`, respSub.Body.String()) + assert.Equal(t, `
<script></script>
`, respSub.Body.String()) }) }) @@ -131,7 +131,7 @@ func TestExternalMarkupRenderer(t *testing.T) { t.Run("HTMLContentWithExternalRenderIframeHelper", func(t *testing.T) { req := NewRequest(t, "GET", "/user2/repo1/render/branch/master/html.no-sanitizer") respSub := MakeRequest(t, req, http.StatusOK) - assert.Equal(t, ``, respSub.Body.String()) + assert.Equal(t, ``, respSub.Body.String()) assert.Equal(t, "frame-src 'self'", respSub.Header().Get("Content-Security-Policy")) }) }) From 1b18e4d14a247a6ac7ab1261dda0c934e6afc175 Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 27 Mar 2026 10:20:56 +0100 Subject: [PATCH 075/102] Fix test function name to satisfy Go naming convention TestgetAssetPathFallback -> TestGetAssetPathFallback Co-Authored-By: Claude (Opus 4.6) --- modules/public/manifest_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/public/manifest_test.go b/modules/public/manifest_test.go index f17a596e03775..3fea4f91189fa 100644 --- a/modules/public/manifest_test.go +++ b/modules/public/manifest_test.go @@ -61,7 +61,7 @@ func TestParseManifest(t *testing.T) { assert.Empty(t, paths["js/chunk.js"]) } -func TestgetAssetPathFallback(t *testing.T) { +func TestGetAssetPathFallback(t *testing.T) { // When manifest is not loaded, getAssetPath should return the input as-is old := manifestData.Load() manifestData.Store(&manifestDataStruct{paths: make(map[string]string)}) From 82207607c4976d553d3bec8464bf9bce1768a186 Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 27 Mar 2026 11:46:49 +0100 Subject: [PATCH 076/102] Use manifest for theme name resolution, support custom themes in dev mode Resolve content-hashed theme filenames via the Vite manifest instead of guessing with string manipulation. Custom themes not in the manifest skip hash stripping entirely since they never have content hashes. In Vite dev mode, only redirect built-in themes to the Vite source directory. Custom themes in custom/public/assets/css/ are served via the regular static file handler. Co-Authored-By: Claude (Opus 4.6) --- modules/public/manifest.go | 27 ++++++++++++++++++++++----- modules/public/manifest_test.go | 12 ++++++++++-- modules/public/vitedev.go | 8 +++++++- services/webtheme/webtheme.go | 20 ++++++++------------ 4 files changed, 47 insertions(+), 20 deletions(-) diff --git a/modules/public/manifest.go b/modules/public/manifest.go index ef01bc62ce765..0b3c9f60ab749 100644 --- a/modules/public/manifest.go +++ b/modules/public/manifest.go @@ -23,7 +23,8 @@ type manifestEntry struct { } type manifestDataStruct struct { - paths map[string]string + paths map[string]string // unhashed path -> hashed path + names map[string]string // hashed path -> entry name modTime int64 checkTime time.Time } @@ -35,14 +36,15 @@ var ( const manifestPath = "assets/.vite/manifest.json" -func parseManifest(data []byte) map[string]string { +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 + return nil, nil } paths := make(map[string]string) + names := make(map[string]string) for _, entry := range manifest { if !entry.IsEntry || entry.Name == "" { continue @@ -52,13 +54,15 @@ func parseManifest(data []byte) map[string]string { 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 + return paths, names } func reloadManifest() *manifestDataStruct { @@ -93,8 +97,10 @@ func reloadManifest() *manifestDataStruct { log.Error("Failed to read frontend manifest: %v", err) return data } + paths, names := parseManifest(manifestContent) data = &manifestDataStruct{ - paths: parseManifest(manifestContent), + paths: paths, + names: names, modTime: fi.ModTime().UnixNano(), checkTime: now, } @@ -126,3 +132,14 @@ func getAssetPath(name string) string { } return name } + +// AssetName returns the unhashed entry name for a content-hashed asset path. +// Example: AssetName("css/theme-gitea-dark.CyAaQnn5.css") returns "theme-gitea-dark" +// Returns empty string if the path is not found in the manifest. +func AssetName(hashedPath string) string { + getManifestPaths() // ensure manifest is loaded + if data := manifestData.Load(); data != nil { + return data.names[hashedPath] + } + return "" +} diff --git a/modules/public/manifest_test.go b/modules/public/manifest_test.go index 3fea4f91189fa..8773be61ae0ed 100644 --- a/modules/public/manifest_test.go +++ b/modules/public/manifest_test.go @@ -43,7 +43,7 @@ func TestParseManifest(t *testing.T) { } }`) - paths := parseManifest(manifest) + paths, names := parseManifest(manifest) // JS entries assert.Equal(t, "js/index.C6Z2MRVQ.js", paths["js/index.js"]) @@ -59,12 +59,20 @@ func TestParseManifest(t *testing.T) { // Non-entry chunks should not be included assert.Empty(t, paths["js/chunk.js"]) + + // Names: hashed path -> entry name + assert.Equal(t, "index", names["js/index.C6Z2MRVQ.js"]) + assert.Equal(t, "index", names["css/index.B3zrQPqD.css"]) + assert.Equal(t, "swagger", names["js/swagger.SujiEmYM.js"]) + assert.Equal(t, "swagger", names["css/swagger._-APWT_3.css"]) + assert.Equal(t, "theme-gitea-dark", names["css/theme-gitea-dark.CyAaQnn5.css"]) + assert.Equal(t, "sharedworker", names["js/sharedworker.Dug1twio.js"]) } func TestGetAssetPathFallback(t *testing.T) { // When manifest is not loaded, getAssetPath should return the input as-is old := manifestData.Load() - manifestData.Store(&manifestDataStruct{paths: make(map[string]string)}) + manifestData.Store(&manifestDataStruct{paths: make(map[string]string), names: make(map[string]string)}) defer func() { manifestData.Store(old) }() assert.Equal(t, "js/index.js", getAssetPath("js/index.js")) diff --git a/modules/public/vitedev.go b/modules/public/vitedev.go index ad3eb2cd36440..a6c2b58e2e315 100644 --- a/modules/public/vitedev.go +++ b/modules/public/vitedev.go @@ -92,7 +92,13 @@ func AssetPath(name string) string { func viteDevSourceURL(name string) string { if strings.HasPrefix(name, "css/theme-") { - return setting.AppSubURL + "/web_src/css/themes/" + strings.TrimPrefix(name, "css/") + // Only redirect built-in themes to Vite source; custom themes are served from custom/public/assets/css/ + themeFile := strings.TrimPrefix(name, "css/") + srcPath := filepath.Join(setting.StaticRootPath, "web_src/css/themes", themeFile) + if _, err := os.Stat(srcPath); err == nil { + return setting.AppSubURL + "/web_src/css/themes/" + themeFile + } + return "" } if strings.HasPrefix(name, "css/") { return setting.AppSubURL + "/web_src/" + name diff --git a/services/webtheme/webtheme.go b/services/webtheme/webtheme.go index b69fd9858ebb5..e5b2adc9a8d07 100644 --- a/services/webtheme/webtheme.go +++ b/services/webtheme/webtheme.go @@ -108,21 +108,17 @@ func parseThemeMetaInfoToMap(cssContent string) map[string]string { return m } -// stripContentHash removes a Vite content hash suffix from a name. -// e.g. "gitea-dark.CyAaQnn5" -> "gitea-dark" -// It might be wrong when user's theme name is like "my-theme-1.2.css", fortunately it is not a serious problem at the moment -// If we'd like to "fix" it, we can add a "hash prefix" to the Vite assets like "index.h~123456.css", then in most cases we do best guess to strip the hash correctly. -func stripContentHash(name string) string { - if i := strings.LastIndex(name, "."); i > 0 { - return name[:i] - } - return name -} - func defaultThemeMetaInfoByFileName(fileName string) *ThemeMetaInfo { + internalName := strings.TrimSuffix(strings.TrimPrefix(fileName, fileNamePrefix), fileNameSuffix) + // For built-in themes, the manifest knows the unhashed entry name (e.g. "theme-gitea-dark") + // which lets us correctly strip the content hash without guessing. + // Custom themes are not in the manifest and never have content hashes. + if name := public.AssetName("css/" + fileName); name != "" { + internalName = strings.TrimPrefix(name, fileNamePrefix) + } themeInfo := &ThemeMetaInfo{ FileName: fileName, - InternalName: stripContentHash(strings.TrimSuffix(strings.TrimPrefix(fileName, fileNamePrefix), fileNameSuffix)), + InternalName: internalName, } themeInfo.DisplayName = themeInfo.InternalName return themeInfo From 084ffd5490c8132b475483b9509a472c8e4a5dc0 Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 27 Mar 2026 12:07:40 +0100 Subject: [PATCH 077/102] Fix shared worker path, rename back to eventsource.sharedworker Resolve merge conflict with origin/main's UserEventsSharedWorker by using sharedWorkerPath from window.config instead of webpack globals. Rename shared worker file back to eventsource.sharedworker.ts. Fix EXECUTABLE_E2E dependency on FRONTEND_DEST instead of WEBPACK_DEST. Rename VITE_DEV_SERVER_PORT to FRONTEND_DEV_SERVER_PORT. Remove obsolete webpack gitignore entry. Co-Authored-By: Claude (Opus 4.6) --- .gitignore | 2 -- Makefile | 2 +- modules/public/manifest_test.go | 12 ++++++------ modules/public/vitedev.go | 4 ++-- templates/base/head_script.tmpl | 2 +- vite.config.ts | 4 ++-- .../{sharedworker.ts => eventsource.sharedworker.ts} | 0 web_src/js/modules/worker.ts | 4 ++-- 8 files changed, 14 insertions(+), 16 deletions(-) rename web_src/js/features/{sharedworker.ts => eventsource.sharedworker.ts} (100%) diff --git a/.gitignore b/.gitignore index e1f68f887e640..019ee94c7a3bd 100644 --- a/.gitignore +++ b/.gitignore @@ -88,8 +88,6 @@ cpu.out /VERSION /.air -# Files and folders that were previously generated -/public/assets/img/webpack # Snapcraft /gitea_a*.txt diff --git a/Makefile b/Makefile index 3630e1a70db9e..853d08649dd78 100644 --- a/Makefile +++ b/Makefile @@ -671,7 +671,7 @@ ifneq ($(and $(STATIC),$(findstring pam,$(TAGS))),) endif CGO_ENABLED="$(CGO_ENABLED)" CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) build $(GOFLAGS) $(EXTRA_GOFLAGS) -tags '$(TAGS)' -ldflags '-s -w $(EXTLDFLAGS) $(LDFLAGS)' -o $@ -$(EXECUTABLE_E2E): $(GO_SOURCES) $(WEBPACK_DEST) +$(EXECUTABLE_E2E): $(GO_SOURCES) $(FRONTEND_DEST) CGO_ENABLED=1 $(GO) build $(GOFLAGS) $(EXTRA_GOFLAGS) -tags '$(TEST_TAGS)' -ldflags '-s -w $(EXTLDFLAGS) $(LDFLAGS)' -o $@ .PHONY: release diff --git a/modules/public/manifest_test.go b/modules/public/manifest_test.go index 8773be61ae0ed..447f1132f24e4 100644 --- a/modules/public/manifest_test.go +++ b/modules/public/manifest_test.go @@ -31,10 +31,10 @@ func TestParseManifest(t *testing.T) { "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", + "web_src/js/features/eventsource.sharedworker.ts": { + "file": "js/eventsource.sharedworker.Dug1twio.js", + "name": "eventsource.sharedworker", + "src": "web_src/js/features/eventsource.sharedworker.ts", "isEntry": true }, "_chunk.js": { @@ -48,7 +48,7 @@ func TestParseManifest(t *testing.T) { // 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"]) + assert.Equal(t, "js/eventsource.sharedworker.Dug1twio.js", paths["js/eventsource.sharedworker.js"]) // Associated CSS from JS entries assert.Equal(t, "css/index.B3zrQPqD.css", paths["css/index.css"]) @@ -66,7 +66,7 @@ func TestParseManifest(t *testing.T) { assert.Equal(t, "swagger", names["js/swagger.SujiEmYM.js"]) assert.Equal(t, "swagger", names["css/swagger._-APWT_3.css"]) assert.Equal(t, "theme-gitea-dark", names["css/theme-gitea-dark.CyAaQnn5.css"]) - assert.Equal(t, "sharedworker", names["js/sharedworker.Dug1twio.js"]) + assert.Equal(t, "eventsource.sharedworker", names["js/eventsource.sharedworker.Dug1twio.js"]) } func TestGetAssetPathFallback(t *testing.T) { diff --git a/modules/public/vitedev.go b/modules/public/vitedev.go index a6c2b58e2e315..b19600652c639 100644 --- a/modules/public/vitedev.go +++ b/modules/public/vitedev.go @@ -103,8 +103,8 @@ func viteDevSourceURL(name string) string { if strings.HasPrefix(name, "css/") { return setting.AppSubURL + "/web_src/" + name } - if name == "js/sharedworker.js" { - return setting.AppSubURL + "/web_src/js/features/sharedworker.ts" + if name == "js/eventsource.sharedworker.js" { + return setting.AppSubURL + "/web_src/js/features/eventsource.sharedworker.ts" } if name == "js/iife.js" { return setting.AppSubURL + "/__vite_iife.js" diff --git a/templates/base/head_script.tmpl b/templates/base/head_script.tmpl index 8f912fc897d85..a5c31ef590d4c 100644 --- a/templates/base/head_script.tmpl +++ b/templates/base/head_script.tmpl @@ -16,7 +16,7 @@ If you introduce mistakes in it, Gitea JavaScript code wouldn't run correctly. notificationSettings: {{NotificationSettings}}, {{/*a map provided by NewFuncMap in helper.go*/}} enableTimeTracking: {{EnableTimetracking}}, mermaidMaxSourceCharacters: {{MermaidMaxSourceCharacters}}, - sharedWorkerPath: '{{AssetPath "js/sharedworker.js"}}', + sharedWorkerPath: '{{AssetPath "js/eventsource.sharedworker.js"}}', {{/* this global i18n object should only contain general texts. for specialized texts, it should be provided inside the related modules by: (1) API response (2) HTML data-attribute (3) PageData */}} i18n: { copy_success: {{ctx.Locale.Tr "copy_success"}}, diff --git a/vite.config.ts b/vite.config.ts index d09e2d1bac7bd..829cfa32cd14f 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -163,7 +163,7 @@ function filterCssUrlPlugin(): Plugin { }; } -const viteDevServerPort = Number(env.VITE_DEV_SERVER_PORT) || 3001; +const viteDevServerPort = Number(env.FRONTEND_DEV_SERVER_PORT) || 3001; const viteDevPortFilePath = join(outDir, '.vite', 'dev-port'); // Write the Vite dev server's actual port to a file so the Go server can discover it for proxying. @@ -210,7 +210,7 @@ export default defineConfig(commonViteOpts({ index: join(import.meta.dirname, 'web_src/js/index.ts'), swagger: join(import.meta.dirname, 'web_src/js/standalone/swagger.ts'), 'external-render-iframe': join(import.meta.dirname, 'web_src/js/standalone/external-render-iframe.ts'), - sharedworker: join(import.meta.dirname, 'web_src/js/features/sharedworker.ts'), + 'eventsource.sharedworker': join(import.meta.dirname, 'web_src/js/features/eventsource.sharedworker.ts'), ...(!isProduction && { devtest: join(import.meta.dirname, 'web_src/js/standalone/devtest.ts'), }), diff --git a/web_src/js/features/sharedworker.ts b/web_src/js/features/eventsource.sharedworker.ts similarity index 100% rename from web_src/js/features/sharedworker.ts rename to web_src/js/features/eventsource.sharedworker.ts diff --git a/web_src/js/modules/worker.ts b/web_src/js/modules/worker.ts index b730e30bb2e4e..1a9e9a588da22 100644 --- a/web_src/js/modules/worker.ts +++ b/web_src/js/modules/worker.ts @@ -1,11 +1,11 @@ -const {appSubUrl, assetVersionEncoded} = window.config; +const {appSubUrl, sharedWorkerPath} = window.config; export class UserEventsSharedWorker { sharedWorker: SharedWorker; // options can be either a string (the debug name of the worker) or an object of type WorkerOptions constructor(options?: string | WorkerOptions) { - const worker = new SharedWorker(`${window.__webpack_public_path__}js/eventsource.sharedworker.js?v=${assetVersionEncoded}`, options); + const worker = new SharedWorker(sharedWorkerPath, options); this.sharedWorker = worker; worker.addEventListener('error', (event) => { console.error('worker error', event); From 836c1bf0da0a8b849f61db6e8aad3af9c726809f Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 27 Mar 2026 12:09:33 +0100 Subject: [PATCH 078/102] Refactor getManifestPaths to getManifestData Co-Authored-By: Claude (Opus 4.6) --- modules/public/manifest.go | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/modules/public/manifest.go b/modules/public/manifest.go index 0b3c9f60ab749..cb7b175aa2a87 100644 --- a/modules/public/manifest.go +++ b/modules/public/manifest.go @@ -108,7 +108,7 @@ func reloadManifest() *manifestDataStruct { return data } -func getManifestPaths() map[string]string { +func getManifestData() *manifestDataStruct { data := manifestData.Load() // In production the manifest is immutable (embedded in the binary). @@ -116,19 +116,17 @@ func getManifestPaths() map[string]string { if data == nil || !setting.IsProd { data = reloadManifest() } - if data != nil { - return data.paths - } - return nil + return data } // getAssetPath resolves an unhashed asset path to its content-hashed path from the frontend 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 + if data := getManifestData(); data != nil { + if p, ok := data.paths[name]; ok { + return p + } } return name } @@ -137,8 +135,7 @@ func getAssetPath(name string) string { // Example: AssetName("css/theme-gitea-dark.CyAaQnn5.css") returns "theme-gitea-dark" // Returns empty string if the path is not found in the manifest. func AssetName(hashedPath string) string { - getManifestPaths() // ensure manifest is loaded - if data := manifestData.Load(); data != nil { + if data := getManifestData(); data != nil { return data.names[hashedPath] } return "" From 98c081a8ea05c75b6a2258361045dae3febbab6b Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 27 Mar 2026 12:26:06 +0100 Subject: [PATCH 079/102] remove unneeded arg --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 853d08649dd78..824bfac10c682 100644 --- a/Makefile +++ b/Makefile @@ -381,7 +381,7 @@ watch: ## watch everything and continuously rebuild .PHONY: watch-frontend watch-frontend: node_modules ## start vite dev server for frontend - NODE_ENV=development $(NODE_VARS) pnpm exec vite dev + NODE_ENV=development $(NODE_VARS) pnpm exec vite .PHONY: watch-backend watch-backend: ## watch backend files and continuously rebuild From 937ee84d67b83a9c7798b3ecf6fde49c2ff54971 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Fri, 27 Mar 2026 19:32:10 +0800 Subject: [PATCH 080/102] clarify hashed path, origin path, asset path, asset name --- modules/public/manifest.go | 48 ++++++++++++++++++++------------- modules/public/manifest_test.go | 8 +++--- modules/public/vitedev.go | 13 --------- services/webtheme/webtheme.go | 2 +- 4 files changed, 35 insertions(+), 36 deletions(-) diff --git a/modules/public/manifest.go b/modules/public/manifest.go index cb7b175aa2a87..a797b4829dd80 100644 --- a/modules/public/manifest.go +++ b/modules/public/manifest.go @@ -65,11 +65,11 @@ func parseManifest(data []byte) (map[string]string, map[string]string) { return paths, names } -func reloadManifest() *manifestDataStruct { +func reloadManifest(existingData *manifestDataStruct) *manifestDataStruct { now := time.Now() - data := manifestData.Load() + data := existingData if data != nil && now.Sub(data.checkTime) < time.Second { - // a single request triggers multiple calls to getAssetPath + // a single request triggers multiple calls to getHashedPath // do not check the manifest file too frequently return data } @@ -114,29 +114,41 @@ func getManifestData() *manifestDataStruct { // 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 = reloadManifest(data) + } + if data == nil { + data = &manifestDataStruct{} } return data } -// getAssetPath resolves an unhashed asset path to its content-hashed path from the frontend manifest. -// Example: getAssetPath("js/index.js") returns "js/index.C6Z2MRVQ.js" +// 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 getAssetPath(name string) string { - if data := getManifestData(); data != nil { - if p, ok := data.paths[name]; ok { - return p +func getHashedPath(originPath string) string { + data := getManifestData() + if p, ok := data.paths[originPath]; ok { + return p + } + return originPath +} + +// AssetPath returns the asset path (full URL path) for a frontend asset. +// 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 AssetPath(originPath string) string { + if IsViteDevMode() { + if src := viteDevSourceURL(originPath); src != "" { + return src } } - return name + return setting.StaticURLPrefix + "/assets/" + getHashedPath(originPath) } -// AssetName returns the unhashed entry name for a content-hashed asset path. -// Example: AssetName("css/theme-gitea-dark.CyAaQnn5.css") returns "theme-gitea-dark" +// 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 AssetName(hashedPath string) string { - if data := getManifestData(); data != nil { - return data.names[hashedPath] - } - return "" +func AssetNameFromHashedPath(hashedPath string) string { + return getManifestData().names[hashedPath] } diff --git a/modules/public/manifest_test.go b/modules/public/manifest_test.go index 447f1132f24e4..6e362b3e171e0 100644 --- a/modules/public/manifest_test.go +++ b/modules/public/manifest_test.go @@ -70,11 +70,11 @@ func TestParseManifest(t *testing.T) { } func TestGetAssetPathFallback(t *testing.T) { - // When manifest is not loaded, getAssetPath should return the input as-is + // When manifest is not loaded, getHashedPath should return the input as-is old := manifestData.Load() - manifestData.Store(&manifestDataStruct{paths: make(map[string]string), names: make(map[string]string)}) + manifestData.Store(&manifestDataStruct{paths: make(map[string]string), origins: make(map[string]string)}) defer func() { manifestData.Store(old) }() - assert.Equal(t, "js/index.js", getAssetPath("js/index.js")) - assert.Equal(t, "css/theme-gitea-dark.css", getAssetPath("css/theme-gitea-dark.css")) + assert.Equal(t, "js/index.js", getHashedPath("js/index.js")) + assert.Equal(t, "css/theme-gitea-dark.css", getHashedPath("css/theme-gitea-dark.css")) } diff --git a/modules/public/vitedev.go b/modules/public/vitedev.go index b19600652c639..a54555143b986 100644 --- a/modules/public/vitedev.go +++ b/modules/public/vitedev.go @@ -77,19 +77,6 @@ func IsViteDevMode() bool { return err == nil } -// AssetPath returns the rooted path for a frontend asset. -// 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 AssetPath(name string) string { - if IsViteDevMode() { - if src := viteDevSourceURL(name); src != "" { - return src - } - } - return setting.StaticURLPrefix + "/assets/" + getAssetPath(name) -} - func viteDevSourceURL(name string) string { if strings.HasPrefix(name, "css/theme-") { // Only redirect built-in themes to Vite source; custom themes are served from custom/public/assets/css/ diff --git a/services/webtheme/webtheme.go b/services/webtheme/webtheme.go index e5b2adc9a8d07..2f3d06d78067e 100644 --- a/services/webtheme/webtheme.go +++ b/services/webtheme/webtheme.go @@ -113,7 +113,7 @@ func defaultThemeMetaInfoByFileName(fileName string) *ThemeMetaInfo { // For built-in themes, the manifest knows the unhashed entry name (e.g. "theme-gitea-dark") // which lets us correctly strip the content hash without guessing. // Custom themes are not in the manifest and never have content hashes. - if name := public.AssetName("css/" + fileName); name != "" { + if name := public.AssetNameFromHashedPath("css/" + fileName); name != "" { internalName = strings.TrimPrefix(name, fileNamePrefix) } themeInfo := &ThemeMetaInfo{ From 55e98405a447c1ff98ab6ebad149b1d38c349fe6 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Fri, 27 Mar 2026 19:35:53 +0800 Subject: [PATCH 081/102] clean up --- modules/public/manifest.go | 6 ++---- modules/public/vitedev.go | 13 ++++++++----- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/modules/public/manifest.go b/modules/public/manifest.go index a797b4829dd80..c8c41cbd712e0 100644 --- a/modules/public/manifest.go +++ b/modules/public/manifest.go @@ -138,10 +138,8 @@ func getHashedPath(originPath string) string { // so the reverse proxy serves them from the Vite dev server. // In production, it resolves the content-hashed path from the manifest. func AssetPath(originPath string) string { - if IsViteDevMode() { - if src := viteDevSourceURL(originPath); src != "" { - return src - } + if src := viteDevSourceURL(originPath); src != "" { + return src } return setting.StaticURLPrefix + "/assets/" + getHashedPath(originPath) } diff --git a/modules/public/vitedev.go b/modules/public/vitedev.go index a54555143b986..ce8e8f4e2ab96 100644 --- a/modules/public/vitedev.go +++ b/modules/public/vitedev.go @@ -56,7 +56,7 @@ func getViteDevProxy() *httputil.ReverseProxy { // It is registered as middleware in non-production mode and lazily discovers // the Vite dev server port from the port file written by the viteDevServerPortPlugin. func ViteDevMiddleware(resp http.ResponseWriter, req *http.Request) { - if !IsViteDevRequest(req) { + if !isViteDevRequest(req) { return } proxy := getViteDevProxy() @@ -66,9 +66,9 @@ func ViteDevMiddleware(resp http.ResponseWriter, req *http.Request) { proxy.ServeHTTP(resp, req) } -// IsViteDevMode returns true if the Vite dev server port file exists. +// isViteDevMode returns true if the Vite dev server port file exists. // In production mode, the result is cached after the first check. -func IsViteDevMode() bool { +func isViteDevMode() bool { if setting.IsProd { return false } @@ -78,6 +78,9 @@ func IsViteDevMode() bool { } func viteDevSourceURL(name string) string { + if !isViteDevMode() { + return "" + } if strings.HasPrefix(name, "css/theme-") { // Only redirect built-in themes to Vite source; custom themes are served from custom/public/assets/css/ themeFile := strings.TrimPrefix(name, "css/") @@ -102,13 +105,13 @@ func viteDevSourceURL(name string) string { return "" } -// IsViteDevRequest returns true if the request should be proxied to the Vite dev server. +// isViteDevRequest returns true if the request should be proxied to the Vite dev server. // Vite internal prefixes are defined in the Vite source: // - packages/vite/src/node/constants.ts (/@vite/, /@fs/, /__vite) // - packages/vite/src/shared/constants.ts (/@id/) // - packages/vite/src/node/server/ws.ts (vite-hmr, vite-ping WebSocket protocols) // - packages/vite/src/node/utils.ts (?import, ?raw query params) -func IsViteDevRequest(req *http.Request) bool { +func isViteDevRequest(req *http.Request) bool { wsProtocol := req.Header.Get("Sec-WebSocket-Protocol") if req.Header.Get("Upgrade") == "websocket" && (wsProtocol == "vite-hmr" || wsProtocol == "vite-ping") { return true From 4bf588be21a9d1d1450ef10eb0bf8f5a768cb8eb Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Fri, 27 Mar 2026 19:57:28 +0800 Subject: [PATCH 082/102] fix test --- modules/public/manifest.go | 11 ++- modules/public/manifest_test.go | 131 +++++++++++++++++--------------- 2 files changed, 78 insertions(+), 64 deletions(-) diff --git a/modules/public/manifest.go b/modules/public/manifest.go index c8c41cbd712e0..435ef0accd1af 100644 --- a/modules/public/manifest.go +++ b/modules/public/manifest.go @@ -91,18 +91,21 @@ func reloadManifest(existingData *manifestDataStruct) *manifestDataStruct { 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{ + data := &manifestDataStruct{ paths: paths, names: names, - modTime: fi.ModTime().UnixNano(), - checkTime: now, + modTime: modTime, + checkTime: checkTime, } manifestData.Store(data) return data diff --git a/modules/public/manifest_test.go b/modules/public/manifest_test.go index 6e362b3e171e0..4b3dba2bbd1b5 100644 --- a/modules/public/manifest_test.go +++ b/modules/public/manifest_test.go @@ -5,76 +5,87 @@ package public import ( "testing" + "time" + + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" "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/eventsource.sharedworker.ts": { - "file": "js/eventsource.sharedworker.Dug1twio.js", - "name": "eventsource.sharedworker", - "src": "web_src/js/features/eventsource.sharedworker.ts", - "isEntry": true - }, - "_chunk.js": { - "file": "js/chunk.abc123.js", - "name": "chunk" - } - }`) +func TestViteManifest(t *testing.T) { + defer test.MockVariableValue(&setting.IsProd, true)() - paths, names := parseManifest(manifest) + const testManifest = `{ + "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/eventsource.sharedworker.ts": { + "file": "js/eventsource.sharedworker.Dug1twio.js", + "name": "eventsource.sharedworker", + "src": "web_src/js/features/eventsource.sharedworker.ts", + "isEntry": true + }, + "_chunk.js": { + "file": "js/chunk.abc123.js", + "name": "chunk" + } +}` - // 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/eventsource.sharedworker.Dug1twio.js", paths["js/eventsource.sharedworker.js"]) + t.Run("EmptyManifest", func(t *testing.T) { + storeManifestFromBytes([]byte(``), 0, time.Now()) + assert.Equal(t, "/assets/js/index.js", AssetPath("js/index.js")) + assert.Equal(t, "/assets/css/theme-gitea-dark.css", AssetPath("css/theme-gitea-dark.css")) + assert.Equal(t, "", AssetNameFromHashedPath("css/no-such-file.css")) + }) - // 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"]) + t.Run("ParseManifest", func(t *testing.T) { + storeManifestFromBytes([]byte(testManifest), 0, time.Now()) + paths, names := manifestData.Load().paths, manifestData.Load().names - // CSS-only entries - assert.Equal(t, "css/theme-gitea-dark.CyAaQnn5.css", paths["css/theme-gitea-dark.css"]) + // 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/eventsource.sharedworker.Dug1twio.js", paths["js/eventsource.sharedworker.js"]) - // Non-entry chunks should not be included - assert.Empty(t, paths["js/chunk.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"]) - // Names: hashed path -> entry name - assert.Equal(t, "index", names["js/index.C6Z2MRVQ.js"]) - assert.Equal(t, "index", names["css/index.B3zrQPqD.css"]) - assert.Equal(t, "swagger", names["js/swagger.SujiEmYM.js"]) - assert.Equal(t, "swagger", names["css/swagger._-APWT_3.css"]) - assert.Equal(t, "theme-gitea-dark", names["css/theme-gitea-dark.CyAaQnn5.css"]) - assert.Equal(t, "eventsource.sharedworker", names["js/eventsource.sharedworker.Dug1twio.js"]) -} + // 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, getHashedPath should return the input as-is - old := manifestData.Load() - manifestData.Store(&manifestDataStruct{paths: make(map[string]string), origins: make(map[string]string)}) - defer func() { manifestData.Store(old) }() + // Names: hashed path -> entry name + assert.Equal(t, "index", names["js/index.C6Z2MRVQ.js"]) + assert.Equal(t, "index", names["css/index.B3zrQPqD.css"]) + assert.Equal(t, "swagger", names["js/swagger.SujiEmYM.js"]) + assert.Equal(t, "swagger", names["css/swagger._-APWT_3.css"]) + assert.Equal(t, "theme-gitea-dark", names["css/theme-gitea-dark.CyAaQnn5.css"]) + assert.Equal(t, "eventsource.sharedworker", names["js/eventsource.sharedworker.Dug1twio.js"]) - assert.Equal(t, "js/index.js", getHashedPath("js/index.js")) - assert.Equal(t, "css/theme-gitea-dark.css", getHashedPath("css/theme-gitea-dark.css")) + // Test Asset related functions + assert.Equal(t, "/assets/js/index.C6Z2MRVQ.js", AssetPath("js/index.js")) + assert.Equal(t, "/assets/css/theme-gitea-dark.CyAaQnn5.css", AssetPath("css/theme-gitea-dark.css")) + assert.Equal(t, "theme-gitea-dark", AssetNameFromHashedPath("css/theme-gitea-dark.CyAaQnn5.css")) + }) } From 31e833af9bc826c96465932650320f5514927de0 Mon Sep 17 00:00:00 2001 From: silverwind Date: Sat, 28 Mar 2026 10:39:53 +0100 Subject: [PATCH 083/102] Clean up isViteDevRequest: remove ?raw, keep ?import MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove ?raw query param check — Gitea uses vite-string-plugin instead. Keep ?import — only added by Vite's transform pipeline for non-JS/CSS imports (e.g. SVG, JSON files), safe and required for proxying. Co-Authored-By: Claude (Opus 4.6) --- modules/public/vitedev.go | 31 ++++++++++++------------------- 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/modules/public/vitedev.go b/modules/public/vitedev.go index ce8e8f4e2ab96..da5e1f9825c24 100644 --- a/modules/public/vitedev.go +++ b/modules/public/vitedev.go @@ -106,30 +106,23 @@ func viteDevSourceURL(name string) string { } // isViteDevRequest returns true if the request should be proxied to the Vite dev server. -// Vite internal prefixes are defined in the Vite source: -// - packages/vite/src/node/constants.ts (/@vite/, /@fs/, /__vite) -// - packages/vite/src/shared/constants.ts (/@id/) -// - packages/vite/src/node/server/ws.ts (vite-hmr, vite-ping WebSocket protocols) -// - packages/vite/src/node/utils.ts (?import, ?raw query params) +// Ref: Vite source packages/vite/src/node/constants.ts and packages/vite/src/shared/constants.ts func isViteDevRequest(req *http.Request) bool { - wsProtocol := req.Header.Get("Sec-WebSocket-Protocol") - if req.Header.Get("Upgrade") == "websocket" && (wsProtocol == "vite-hmr" || wsProtocol == "vite-ping") { - return true + if req.Header.Get("Upgrade") == "websocket" { + wsProtocol := req.Header.Get("Sec-WebSocket-Protocol") + return wsProtocol == "vite-hmr" || wsProtocol == "vite-ping" } path := req.URL.Path - if strings.HasPrefix(path, "/@vite/") || - strings.HasPrefix(path, "/@fs/") || - strings.HasPrefix(path, "/@id/") || - strings.HasPrefix(path, "/__vite") || - strings.HasPrefix(path, "/node_modules/") || - strings.HasPrefix(path, "/web_src/") { - return true - } - query := req.URL.Query() - if _, ok := query["import"]; ok { + if strings.HasPrefix(path, "/@vite/") || // HMR client + strings.HasPrefix(path, "/@fs/") || // out-of-root file access + strings.HasPrefix(path, "/@id/") || // virtual modules + strings.HasPrefix(path, "/__vite") || // ping endpoint, iife + strings.HasPrefix(path, "/node_modules/") || // optimized deps + strings.HasPrefix(path, "/web_src/") { // source files return true } - if _, ok := query["raw"]; ok { + // Vite adds ?import to non-JS/CSS imports (e.g. SVG, JSON files) + if _, ok := req.URL.Query()["import"]; ok { return true } return false From f8814228cb9eaeec9dcd2ec75ce8ff5dc041a240 Mon Sep 17 00:00:00 2001 From: silverwind Date: Sat, 28 Mar 2026 10:45:05 +0100 Subject: [PATCH 084/102] Rename AssetPath to AssetURI The function may return a relative path or a full URL depending on the StaticURLPrefix setting, so AssetURI is more accurate. Co-Authored-By: Claude (Opus 4.6) --- modules/markup/external/openapi.go | 4 ++-- modules/markup/render.go | 4 ++-- modules/public/manifest.go | 5 +++-- modules/public/manifest_test.go | 8 ++++---- modules/templates/helper.go | 2 +- templates/base/footer.tmpl | 2 +- templates/base/head_script.tmpl | 4 ++-- templates/base/head_style.tmpl | 4 ++-- templates/devtest/devtest-footer.tmpl | 2 +- templates/devtest/devtest-header.tmpl | 2 +- templates/status/500.tmpl | 2 +- templates/swagger/ui.tmpl | 4 ++-- tests/integration/markup_external_test.go | 4 ++-- 13 files changed, 24 insertions(+), 23 deletions(-) diff --git a/modules/markup/external/openapi.go b/modules/markup/external/openapi.go index e74f53250b2b9..de06e7dac7023 100644 --- a/modules/markup/external/openapi.go +++ b/modules/markup/external/openapi.go @@ -69,10 +69,10 @@ func (p *openAPIRenderer) Render(ctx *markup.RenderContext, input io.Reader, out `, - public.AssetPath("css/swagger.css"), + public.AssetURI("css/swagger.css"), html.EscapeString(ctx.RenderOptions.RelativePath), html.EscapeString(util.UnsafeBytesToString(content)), - public.AssetPath("js/swagger.js"), + public.AssetURI("js/swagger.js"), )) return err } diff --git a/modules/markup/render.go b/modules/markup/render.go index e7dc1120133ba..c0d44c72fcca1 100644 --- a/modules/markup/render.go +++ b/modules/markup/render.go @@ -238,8 +238,8 @@ 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 := public.AssetPath("css/external-render-iframe.css") - extraScriptSrc := public.AssetPath("js/external-render-iframe.js") + extraStyleHref := public.AssetURI("css/external-render-iframe.css") + extraScriptSrc := public.AssetURI("js/external-render-iframe.js") // "`, extraScriptSrc, extraStyleHref) } diff --git a/modules/public/manifest.go b/modules/public/manifest.go index 435ef0accd1af..77e8959967221 100644 --- a/modules/public/manifest.go +++ b/modules/public/manifest.go @@ -136,11 +136,12 @@ func getHashedPath(originPath string) string { return originPath } -// AssetPath returns the asset path (full URL path) for a frontend asset. +// 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 AssetPath(originPath string) string { +func AssetURI(originPath string) string { if src := viteDevSourceURL(originPath); src != "" { return src } diff --git a/modules/public/manifest_test.go b/modules/public/manifest_test.go index 4b3dba2bbd1b5..20a2232cf3896 100644 --- a/modules/public/manifest_test.go +++ b/modules/public/manifest_test.go @@ -51,8 +51,8 @@ func TestViteManifest(t *testing.T) { t.Run("EmptyManifest", func(t *testing.T) { storeManifestFromBytes([]byte(``), 0, time.Now()) - assert.Equal(t, "/assets/js/index.js", AssetPath("js/index.js")) - assert.Equal(t, "/assets/css/theme-gitea-dark.css", AssetPath("css/theme-gitea-dark.css")) + assert.Equal(t, "/assets/js/index.js", AssetURI("js/index.js")) + assert.Equal(t, "/assets/css/theme-gitea-dark.css", AssetURI("css/theme-gitea-dark.css")) assert.Equal(t, "", AssetNameFromHashedPath("css/no-such-file.css")) }) @@ -84,8 +84,8 @@ func TestViteManifest(t *testing.T) { assert.Equal(t, "eventsource.sharedworker", names["js/eventsource.sharedworker.Dug1twio.js"]) // Test Asset related functions - assert.Equal(t, "/assets/js/index.C6Z2MRVQ.js", AssetPath("js/index.js")) - assert.Equal(t, "/assets/css/theme-gitea-dark.CyAaQnn5.css", AssetPath("css/theme-gitea-dark.css")) + assert.Equal(t, "/assets/js/index.C6Z2MRVQ.js", AssetURI("js/index.js")) + assert.Equal(t, "/assets/css/theme-gitea-dark.CyAaQnn5.css", AssetURI("css/theme-gitea-dark.css")) assert.Equal(t, "theme-gitea-dark", AssetNameFromHashedPath("css/theme-gitea-dark.CyAaQnn5.css")) }) } diff --git a/modules/templates/helper.go b/modules/templates/helper.go index 0936093d48528..1878565038b71 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -93,7 +93,7 @@ func NewFuncMap() template.FuncMap { "AppDomain": func() string { // documented in mail-templates.md return setting.Domain }, - "AssetPath": public.AssetPath, + "AssetURI": public.AssetURI, "ShowFooterTemplateLoadTime": func() bool { return setting.Other.ShowFooterTemplateLoadTime }, diff --git a/templates/base/footer.tmpl b/templates/base/footer.tmpl index 63b630cfa5659..e9b4b2effe9d0 100644 --- a/templates/base/footer.tmpl +++ b/templates/base/footer.tmpl @@ -9,7 +9,7 @@ {{template "custom/body_outer_post" .}} {{template "base/footer_content" .}} - + {{template "custom/footer" .}} diff --git a/templates/base/head_script.tmpl b/templates/base/head_script.tmpl index a5c31ef590d4c..6266b2badf3fd 100644 --- a/templates/base/head_script.tmpl +++ b/templates/base/head_script.tmpl @@ -16,7 +16,7 @@ If you introduce mistakes in it, Gitea JavaScript code wouldn't run correctly. notificationSettings: {{NotificationSettings}}, {{/*a map provided by NewFuncMap in helper.go*/}} enableTimeTracking: {{EnableTimetracking}}, mermaidMaxSourceCharacters: {{MermaidMaxSourceCharacters}}, - sharedWorkerPath: '{{AssetPath "js/eventsource.sharedworker.js"}}', + sharedWorkerPath: '{{AssetURI "js/eventsource.sharedworker.js"}}', {{/* this global i18n object should only contain general texts. for specialized texts, it should be provided inside the related modules by: (1) API response (2) HTML data-attribute (3) PageData */}} i18n: { copy_success: {{ctx.Locale.Tr "copy_success"}}, @@ -31,4 +31,4 @@ If you introduce mistakes in it, Gitea JavaScript code wouldn't run correctly. {{/* in case some pages don't render the pageData, we make sure it is an object to prevent null access */}} window.config.pageData = window.config.pageData || {}; - + diff --git a/templates/base/head_style.tmpl b/templates/base/head_style.tmpl index 6b1a9dee282e7..15fa7ad730cd9 100644 --- a/templates/base/head_style.tmpl +++ b/templates/base/head_style.tmpl @@ -1,2 +1,2 @@ - - + + diff --git a/templates/devtest/devtest-footer.tmpl b/templates/devtest/devtest-footer.tmpl index 46c50428938ba..868136e1948e0 100644 --- a/templates/devtest/devtest-footer.tmpl +++ b/templates/devtest/devtest-footer.tmpl @@ -1,3 +1,3 @@ {{/* TODO: the devtest.js is isolated from index.js, so no module is shared and many index.js functions do not work in devtest.ts */}} - + {{template "base/footer" ctx.RootData}} diff --git a/templates/devtest/devtest-header.tmpl b/templates/devtest/devtest-header.tmpl index 5653481ff3938..a7aebcb7dc825 100644 --- a/templates/devtest/devtest-header.tmpl +++ b/templates/devtest/devtest-header.tmpl @@ -1,5 +1,5 @@ {{template "base/head" ctx.RootData}} - + + diff --git a/tests/integration/markup_external_test.go b/tests/integration/markup_external_test.go index e12b44bc5171e..3d9d7b3969670 100644 --- a/tests/integration/markup_external_test.go +++ b/tests/integration/markup_external_test.go @@ -108,7 +108,7 @@ func TestExternalMarkupRenderer(t *testing.T) { // default sandbox in sub page response assert.Equal(t, "frame-src 'self'; sandbox allow-scripts allow-popups", respSub.Header().Get("Content-Security-Policy")) // FIXME: actually here is a bug (legacy design problem), the "PostProcess" will escape "
<script></script>
`, respSub.Body.String()) + assert.Equal(t, `
<script></script>
`, respSub.Body.String()) }) }) @@ -131,7 +131,7 @@ func TestExternalMarkupRenderer(t *testing.T) { t.Run("HTMLContentWithExternalRenderIframeHelper", func(t *testing.T) { req := NewRequest(t, "GET", "/user2/repo1/render/branch/master/html.no-sanitizer") respSub := MakeRequest(t, req, http.StatusOK) - assert.Equal(t, ``, respSub.Body.String()) + assert.Equal(t, ``, respSub.Body.String()) assert.Equal(t, "frame-src 'self'", respSub.Header().Get("Content-Security-Policy")) }) }) From 85f2c97c918b7c58f577a92a8cbed021e0adbf12 Mon Sep 17 00:00:00 2001 From: silverwind Date: Sat, 28 Mar 2026 10:53:21 +0100 Subject: [PATCH 085/102] Scope ?import check to /assets/ and /public/assets/ paths Co-Authored-By: Claude (Opus 4.6) --- modules/public/vitedev.go | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/modules/public/vitedev.go b/modules/public/vitedev.go index da5e1f9825c24..760f1c7bd7687 100644 --- a/modules/public/vitedev.go +++ b/modules/public/vitedev.go @@ -121,9 +121,13 @@ func isViteDevRequest(req *http.Request) bool { strings.HasPrefix(path, "/web_src/") { // source files return true } - // Vite adds ?import to non-JS/CSS imports (e.g. SVG, JSON files) - if _, ok := req.URL.Query()["import"]; ok { - return true + // Vite adds ?import to non-JS/CSS asset imports: + // - /public/assets/... (e.g. SVG icons from public/assets/img/svg/) + // - /assets/... (e.g. assets/emoji.json) + if strings.HasPrefix(path, "/assets/") || strings.HasPrefix(path, "/public/assets/") { + if _, ok := req.URL.Query()["import"]; ok { + return true + } } return false } From 642f0d33682840fe173ea0c2a590df20b502fbed Mon Sep 17 00:00:00 2001 From: silverwind Date: Sat, 28 Mar 2026 12:09:38 +0100 Subject: [PATCH 086/102] Restore ENABLE_SOURCEMAP=reduced support Port webpack's three-mode sourcemap control to Vite: - true: all sourcemaps (default in development) - reduced: sourcemaps only for index.js and iife.js (default in production) - false: no sourcemaps Co-Authored-By: Claude (Opus 4.6) --- vite.config.ts | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/vite.config.ts b/vite.config.ts index 829cfa32cd14f..360b64296aa5a 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -11,7 +11,17 @@ import licensePlugin from 'rollup-plugin-license'; import type {InlineConfig, Plugin, Rolldown} from 'vite'; const isProduction = env.NODE_ENV !== 'development'; -const enableSourcemap = env.ENABLE_SOURCEMAP ? env.ENABLE_SOURCEMAP === 'true' : !isProduction; + +// ENABLE_SOURCEMAP accepts the following values: +// true - all sourcemaps enabled, the default in development +// reduced - sourcemaps only for index.js, the default in production +// false - all sourcemaps disabled +let enableSourcemap: string; +if ('ENABLE_SOURCEMAP' in env) { + enableSourcemap = ['true', 'false'].includes(env.ENABLE_SOURCEMAP!) ? env.ENABLE_SOURCEMAP! : 'reduced'; +} else { + enableSourcemap = isProduction ? 'reduced' : 'true'; +} const outDir = join(import.meta.dirname, 'public/assets'); const themes: Record = {}; @@ -50,7 +60,7 @@ function commonViteOpts({build, ...other}: InlineConfig): InlineConfig { build: { outDir, emptyOutDir: false, - sourcemap: enableSourcemap, + sourcemap: enableSourcemap !== 'false', target: 'es2020', minify: isProduction ? 'oxc' : false, cssMinify: isProduction ? 'esbuild' : false, @@ -151,6 +161,20 @@ function iifePlugin(): Plugin { }; } +// In reduced sourcemap mode, only keep sourcemaps for main files +function reducedSourcemapPlugin(): Plugin { + return { + name: 'reduced-sourcemap', + apply: 'build', + closeBundle() { + if (enableSourcemap !== 'reduced') return; + for (const file of globSync('{js,css}/*.map', {cwd: outDir})) { + if (!file.startsWith('js/index.') && !file.startsWith('js/iife.')) unlinkSync(join(outDir, file)); + } + }, + }; +} + // Filter out legacy font formats from CSS, keeping only woff2 function filterCssUrlPlugin(): Plugin { return { @@ -252,6 +276,7 @@ export default defineConfig(commonViteOpts({ plugins: [ iifePlugin(), viteDevServerPortPlugin(), + reducedSourcemapPlugin(), filterCssUrlPlugin(), stringPlugin(), vuePlugin({ From 6267fc5b26254c95c5613b778a8bd668a35d49e8 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sat, 28 Mar 2026 19:23:40 +0800 Subject: [PATCH 087/102] remove "/assets/" check, rename sharedWorkerPath to sharedWorkerUri --- modules/public/vitedev.go | 5 ++--- templates/base/head_script.tmpl | 2 +- web_src/js/globals.d.ts | 2 +- web_src/js/modules/worker.ts | 4 ++-- web_src/js/vitest.setup.ts | 2 +- 5 files changed, 7 insertions(+), 8 deletions(-) diff --git a/modules/public/vitedev.go b/modules/public/vitedev.go index 760f1c7bd7687..2293d74542142 100644 --- a/modules/public/vitedev.go +++ b/modules/public/vitedev.go @@ -121,10 +121,9 @@ func isViteDevRequest(req *http.Request) bool { strings.HasPrefix(path, "/web_src/") { // source files return true } - // Vite adds ?import to non-JS/CSS asset imports: + // Vite uses a path relative to project root and adds "?import" to non-JS/CSS asset imports: // - /public/assets/... (e.g. SVG icons from public/assets/img/svg/) - // - /assets/... (e.g. assets/emoji.json) - if strings.HasPrefix(path, "/assets/") || strings.HasPrefix(path, "/public/assets/") { + if strings.HasPrefix(path, "/public/assets/") { if _, ok := req.URL.Query()["import"]; ok { return true } diff --git a/templates/base/head_script.tmpl b/templates/base/head_script.tmpl index 6266b2badf3fd..feff80ccaaf2f 100644 --- a/templates/base/head_script.tmpl +++ b/templates/base/head_script.tmpl @@ -16,7 +16,7 @@ If you introduce mistakes in it, Gitea JavaScript code wouldn't run correctly. notificationSettings: {{NotificationSettings}}, {{/*a map provided by NewFuncMap in helper.go*/}} enableTimeTracking: {{EnableTimetracking}}, mermaidMaxSourceCharacters: {{MermaidMaxSourceCharacters}}, - sharedWorkerPath: '{{AssetURI "js/eventsource.sharedworker.js"}}', + sharedWorkerUri: '{{AssetURI "js/eventsource.sharedworker.js"}}', {{/* this global i18n object should only contain general texts. for specialized texts, it should be provided inside the related modules by: (1) API response (2) HTML data-attribute (3) PageData */}} i18n: { copy_success: {{ctx.Locale.Tr "copy_success"}}, diff --git a/web_src/js/globals.d.ts b/web_src/js/globals.d.ts index 426a43aadac89..2a6f86b65ec07 100644 --- a/web_src/js/globals.d.ts +++ b/web_src/js/globals.d.ts @@ -23,7 +23,7 @@ interface Window { appUrl: string, appSubUrl: string, assetUrlPrefix: string, - sharedWorkerPath: string, + sharedWorkerUri: string, runModeIsProd: boolean, customEmojis: Record, pageData: Record & { diff --git a/web_src/js/modules/worker.ts b/web_src/js/modules/worker.ts index 1a9e9a588da22..64c32fbe81bc2 100644 --- a/web_src/js/modules/worker.ts +++ b/web_src/js/modules/worker.ts @@ -1,11 +1,11 @@ -const {appSubUrl, sharedWorkerPath} = window.config; +const {appSubUrl, sharedWorkerUri} = window.config; export class UserEventsSharedWorker { sharedWorker: SharedWorker; // options can be either a string (the debug name of the worker) or an object of type WorkerOptions constructor(options?: string | WorkerOptions) { - const worker = new SharedWorker(sharedWorkerPath, options); + const worker = new SharedWorker(sharedWorkerUri, options); this.sharedWorker = worker; worker.addEventListener('error', (event) => { console.error('worker error', event); diff --git a/web_src/js/vitest.setup.ts b/web_src/js/vitest.setup.ts index 42a6bd3335bbf..5623075a2775b 100644 --- a/web_src/js/vitest.setup.ts +++ b/web_src/js/vitest.setup.ts @@ -14,7 +14,7 @@ window.config = { appUrl: 'http://localhost:3000/', appSubUrl: '', assetUrlPrefix: '', - sharedWorkerPath: '', + sharedWorkerUri: '', runModeIsProd: true, customEmojis: {}, pageData: {}, From 493064888bf95b3cd249135d2604b1b9c08826ee Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sat, 28 Mar 2026 19:43:14 +0800 Subject: [PATCH 088/102] fix comment --- modules/public/vitedev.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/modules/public/vitedev.go b/modules/public/vitedev.go index 2293d74542142..da28d40706d84 100644 --- a/modules/public/vitedev.go +++ b/modules/public/vitedev.go @@ -121,9 +121,14 @@ func isViteDevRequest(req *http.Request) bool { strings.HasPrefix(path, "/web_src/") { // source files return true } + // Vite uses a path relative to project root and adds "?import" to non-JS/CSS asset imports: - // - /public/assets/... (e.g. SVG icons from public/assets/img/svg/) - if strings.HasPrefix(path, "/public/assets/") { + // - {WebSite}/public/assets/... (e.g. SVG icons from public/assets/img/svg/) + // - {WebSite}/assets/emoji.json: it is an exception for the frontend assets, it is imported by JS. + // - KEEP IN MIND: all static frontend assets are served from "{AssetsFS}/public/assets" to "{WebSite}/assets" by Gitea Web Server + // - "{RepoRoot}/assets/emoji.json" just happens to have the dir name "assets", it is not related to frontend assets + // - BAD DESIGN: indeed it is a "conflicted and polluted name" sample + if path == "/assets/emoji.json" || strings.HasPrefix(path, "/public/assets/") { if _, ok := req.URL.Query()["import"]; ok { return true } From 38c6a56b941d25fc50aa6547c6c69d2e82a70f3e Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sat, 28 Mar 2026 19:49:31 +0800 Subject: [PATCH 089/102] break middleware chain-call when the request should be handled by vite proxy --- modules/public/vitedev.go | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/modules/public/vitedev.go b/modules/public/vitedev.go index da28d40706d84..434d791c82964 100644 --- a/modules/public/vitedev.go +++ b/modules/public/vitedev.go @@ -55,15 +55,18 @@ func getViteDevProxy() *httputil.ReverseProxy { // ViteDevMiddleware proxies matching requests to the Vite dev server. // It is registered as middleware in non-production mode and lazily discovers // the Vite dev server port from the port file written by the viteDevServerPortPlugin. -func ViteDevMiddleware(resp http.ResponseWriter, req *http.Request) { - if !isViteDevRequest(req) { - return - } - proxy := getViteDevProxy() - if proxy == nil { - return - } - proxy.ServeHTTP(resp, req) +func ViteDevMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { + if !isViteDevRequest(req) { + next.ServeHTTP(resp, req) + return + } + proxy := getViteDevProxy() + if proxy == nil { + return + } + proxy.ServeHTTP(resp, req) + }) } // isViteDevMode returns true if the Vite dev server port file exists. From c8dfb5abdab660705aa3f83d31077eb9ed14e5aa Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sat, 28 Mar 2026 20:02:26 +0800 Subject: [PATCH 090/102] fix comment --- modules/public/vitedev.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/modules/public/vitedev.go b/modules/public/vitedev.go index 434d791c82964..3fd79abbbc0b8 100644 --- a/modules/public/vitedev.go +++ b/modules/public/vitedev.go @@ -126,9 +126,10 @@ func isViteDevRequest(req *http.Request) bool { } // Vite uses a path relative to project root and adds "?import" to non-JS/CSS asset imports: - // - {WebSite}/public/assets/... (e.g. SVG icons from public/assets/img/svg/) - // - {WebSite}/assets/emoji.json: it is an exception for the frontend assets, it is imported by JS. - // - KEEP IN MIND: all static frontend assets are served from "{AssetsFS}/public/assets" to "{WebSite}/assets" by Gitea Web Server + // - {WebSite}/public/assets/... (e.g. SVG icons from "{RepoRoot}/public/assets/img/svg/") + // - {WebSite}/assets/emoji.json: it is an exception for the frontend assets, it is imported by JS code, but: + // - KEEP IN MIND: all static frontend assets are served from "{AssetFS}/assets" to "{WebSite}/assets" by Gitea Web Server + // - "{AssetFS}" is a layered filesystem from "{RepoRoot}/public" or embedded assets, and user's custom files in "{CustomPath}/public" // - "{RepoRoot}/assets/emoji.json" just happens to have the dir name "assets", it is not related to frontend assets // - BAD DESIGN: indeed it is a "conflicted and polluted name" sample if path == "/assets/emoji.json" || strings.HasPrefix(path, "/public/assets/") { From bf007fede7c13dde0fcd3cf4bd64929970ead52b Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sat, 28 Mar 2026 20:24:00 +0800 Subject: [PATCH 091/102] mark vite connection as long polling to avoid error logs --- modules/public/vitedev.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/modules/public/vitedev.go b/modules/public/vitedev.go index 3fd79abbbc0b8..ed68eb59c8502 100644 --- a/modules/public/vitedev.go +++ b/modules/public/vitedev.go @@ -14,6 +14,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/web/routing" ) const viteDevPortFile = "public/assets/.vite/dev-port" @@ -56,11 +57,14 @@ func getViteDevProxy() *httputil.ReverseProxy { // It is registered as middleware in non-production mode and lazily discovers // the Vite dev server port from the port file written by the viteDevServerPortPlugin. func ViteDevMiddleware(next http.Handler) http.Handler { + // FIXME: there is a strange error log, not sure why it happens yet + // 2026/03/28 19:50:13 modules/log/misc.go:72:(*loggerToWriter).Write() [I] Unsolicited response received on idle HTTP channel starting with "HTTP/1.1 400 Bad Request\r\n\r\n"; err= return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { if !isViteDevRequest(req) { next.ServeHTTP(resp, req) return } + routing.MarkLongPolling(resp, req) proxy := getViteDevProxy() if proxy == nil { return From 60b8de1728fce37881a85af770f00a55dd4b3e2b Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sat, 28 Mar 2026 20:34:59 +0800 Subject: [PATCH 092/102] fix ViteDevMiddleware --- modules/public/vitedev.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/modules/public/vitedev.go b/modules/public/vitedev.go index ed68eb59c8502..6e5086ae97f24 100644 --- a/modules/public/vitedev.go +++ b/modules/public/vitedev.go @@ -64,11 +64,13 @@ func ViteDevMiddleware(next http.Handler) http.Handler { next.ServeHTTP(resp, req) return } - routing.MarkLongPolling(resp, req) proxy := getViteDevProxy() if proxy == nil { + next.ServeHTTP(resp, req) return } + // TODO: there are too many logs, need to hide them in the future + routing.MarkLongPolling(resp, req) proxy.ServeHTTP(resp, req) }) } From 9345dc1ecf6de8e84f237cdbc2ff5117d431f595 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sat, 28 Mar 2026 20:49:22 +0800 Subject: [PATCH 093/102] avoid vite request log flood --- modules/public/vitedev.go | 1 - modules/web/routing/context.go | 13 ++++++++++ modules/web/routing/logger.go | 37 +++++++++++----------------- modules/web/routing/requestrecord.go | 3 +++ routers/web/web.go | 2 +- 5 files changed, 31 insertions(+), 25 deletions(-) diff --git a/modules/public/vitedev.go b/modules/public/vitedev.go index 6e5086ae97f24..3c5a205ae0576 100644 --- a/modules/public/vitedev.go +++ b/modules/public/vitedev.go @@ -69,7 +69,6 @@ func ViteDevMiddleware(next http.Handler) http.Handler { next.ServeHTTP(resp, req) return } - // TODO: there are too many logs, need to hide them in the future routing.MarkLongPolling(resp, req) proxy.ServeHTTP(resp, req) }) diff --git a/modules/web/routing/context.go b/modules/web/routing/context.go index 838abea158748..e302507bf27dc 100644 --- a/modules/web/routing/context.go +++ b/modules/web/routing/context.go @@ -8,6 +8,7 @@ import ( "net/http" "code.gitea.io/gitea/modules/gtprof" + "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/reqctx" ) @@ -40,6 +41,18 @@ func MarkLongPolling(resp http.ResponseWriter, req *http.Request) { record.lock.Lock() record.isLongPolling = true + record.logLevel = log.TRACE + record.lock.Unlock() +} + +func MarkLogLevelTrace(resp http.ResponseWriter, req *http.Request) { + record, ok := req.Context().Value(contextKey).(*requestRecord) + if !ok { + return + } + + record.lock.Lock() + record.logLevel = log.TRACE record.lock.Unlock() } diff --git a/modules/web/routing/logger.go b/modules/web/routing/logger.go index a6a0e0d51710d..cd139df66bb50 100644 --- a/modules/web/routing/logger.go +++ b/modules/web/routing/logger.go @@ -5,7 +5,6 @@ package routing import ( "net/http" - "strings" "time" "code.gitea.io/gitea/modules/log" @@ -36,17 +35,8 @@ var ( func logPrinter(logger log.Logger) func(trigger Event, record *requestRecord) { const callerName = "HTTPRequest" - logTrace := func(fmt string, args ...any) { - logger.Log(2, &log.Event{Level: log.TRACE, Caller: callerName}, fmt, args...) - } - logInfo := func(fmt string, args ...any) { - logger.Log(2, &log.Event{Level: log.INFO, Caller: callerName}, fmt, args...) - } - logWarn := func(fmt string, args ...any) { - logger.Log(2, &log.Event{Level: log.WARN, Caller: callerName}, fmt, args...) - } - logError := func(fmt string, args ...any) { - logger.Log(2, &log.Event{Level: log.ERROR, Caller: callerName}, fmt, args...) + logRequest := func(level log.Level, fmt string, args ...any) { + logger.Log(2, &log.Event{Level: level, Caller: callerName}, fmt, args...) } return func(trigger Event, record *requestRecord) { if trigger == StartEvent { @@ -57,7 +47,7 @@ func logPrinter(logger log.Logger) func(trigger Event, record *requestRecord) { } // when a request starts, we have no information about the handler function information, we only have the request path req := record.request - logTrace("router: %s %v %s for %s", startMessage, log.ColoredMethod(req.Method), req.RequestURI, req.RemoteAddr) + logRequest(log.TRACE, "router: %s %v %s for %s", startMessage, log.ColoredMethod(req.Method), req.RequestURI, req.RemoteAddr) return } @@ -73,12 +63,12 @@ func logPrinter(logger log.Logger) func(trigger Event, record *requestRecord) { if trigger == StillExecutingEvent { message := slowMessage - logf := logWarn + logLevel := log.WARN if isLongPolling { - logf = logInfo + logLevel = log.INFO message = pollingMessage } - logf("router: %s %v %s for %s, elapsed %v @ %s", + logRequest(logLevel, "router: %s %v %s for %s, elapsed %v @ %s", message, log.ColoredMethod(req.Method), req.RequestURI, req.RemoteAddr, log.ColoredTime(time.Since(record.startTime)), @@ -88,7 +78,7 @@ func logPrinter(logger log.Logger) func(trigger Event, record *requestRecord) { } if panicErr != nil { - logWarn("router: %s %v %s for %s, panic in %v @ %s, err=%v", + logRequest(log.WARN, "router: %s %v %s for %s, panic in %v @ %s, err=%v", failedMessage, log.ColoredMethod(req.Method), req.RequestURI, req.RemoteAddr, log.ColoredTime(time.Since(record.startTime)), @@ -102,21 +92,22 @@ func logPrinter(logger log.Logger) func(trigger Event, record *requestRecord) { if v, ok := record.responseWriter.(types.ResponseStatusProvider); ok { status = v.WrittenStatus() } - logf := logInfo + logLevel := record.logLevel + if logLevel == log.UNDEFINED { + logLevel = log.INFO + } // lower the log level for some specific requests, in most cases these logs are not useful if status > 0 && status < 400 && - strings.HasPrefix(req.RequestURI, "/assets/") /* static assets */ || - req.RequestURI == "/user/events" /* Server-Sent Events (SSE) handler */ || req.RequestURI == "/api/actions/runner.v1.RunnerService/FetchTask" /* Actions Runner polling */ { - logf = logTrace + logLevel = log.TRACE } message := completedMessage if isUnknownHandler { - logf = logError + logLevel = log.ERROR message = unknownHandlerMessage } - logf("router: %s %v %s for %s, %v %v in %v @ %s", + logRequest(logLevel, "router: %s %v %s for %s, %v %v in %v @ %s", message, log.ColoredMethod(req.Method), req.RequestURI, req.RemoteAddr, log.ColoredStatus(status), log.ColoredStatus(status, http.StatusText(status)), log.ColoredTime(time.Since(record.startTime)), diff --git a/modules/web/routing/requestrecord.go b/modules/web/routing/requestrecord.go index 888c3e5c2f4fd..898e2f6d47c63 100644 --- a/modules/web/routing/requestrecord.go +++ b/modules/web/routing/requestrecord.go @@ -7,6 +7,8 @@ import ( "net/http" "sync" "time" + + "code.gitea.io/gitea/modules/log" ) type requestRecord struct { @@ -23,6 +25,7 @@ type requestRecord struct { // mutable fields isLongPolling bool + logLevel log.Level funcInfo *FuncInfo panicError error } diff --git a/routers/web/web.go b/routers/web/web.go index f2f88fc23144d..ec874e4e9768b 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -264,7 +264,7 @@ func Routes() *web.Router { } routes.Head("/", misc.DummyOK) // for health check - doesn't need to be passed through gzip handler - routes.Methods("GET, HEAD, OPTIONS", "/assets/*", optionsCorsHandler(), public.FileHandlerFunc()) + routes.Methods("GET, HEAD, OPTIONS", "/assets/*", routing.MarkLogLevelTrace, optionsCorsHandler(), public.FileHandlerFunc()) routes.Methods("GET, HEAD", "/avatars/*", avatarStorageHandler(setting.Avatar.Storage, "avatars", storage.Avatars)) routes.Methods("GET, HEAD", "/repo-avatars/*", avatarStorageHandler(setting.RepoAvatar.Storage, "repo-avatars", storage.RepoAvatars)) routes.Methods("GET, HEAD", "/apple-touch-icon.png", misc.StaticRedirect("/assets/img/apple-touch-icon.png")) From 90f7a547d7f1bb4fbd84e173a6d466828ed5f217 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sat, 28 Mar 2026 20:57:53 +0800 Subject: [PATCH 094/102] fine tune vite dev proxy --- modules/public/vitedev.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/modules/public/vitedev.go b/modules/public/vitedev.go index 3c5a205ae0576..c1501e2228f50 100644 --- a/modules/public/vitedev.go +++ b/modules/public/vitedev.go @@ -11,6 +11,7 @@ import ( "path/filepath" "strings" "sync/atomic" + "time" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" @@ -42,8 +43,16 @@ func getViteDevProxy() *httputil.ReverseProxy { return nil } + // there is a strange error log (from Golang's HTTP package) + // 2026/03/28 19:50:13 modules/log/misc.go:72:(*loggerToWriter).Write() [I] Unsolicited response received on idle HTTP channel starting with "HTTP/1.1 400 Bad Request\r\n\r\n"; err= + // maybe it is caused by that the Vite dev server doesn't support keep-alive connections? or different keep-alive timeouts? + transport := &http.Transport{ + IdleConnTimeout: 5 * time.Second, + ResponseHeaderTimeout: 5 * time.Second, + } log.Info("Proxying Vite dev server requests to %s", target) proxy := &httputil.ReverseProxy{ + Transport: transport, Rewrite: func(r *httputil.ProxyRequest) { r.SetURL(target) r.Out.Host = target.Host @@ -57,8 +66,6 @@ func getViteDevProxy() *httputil.ReverseProxy { // It is registered as middleware in non-production mode and lazily discovers // the Vite dev server port from the port file written by the viteDevServerPortPlugin. func ViteDevMiddleware(next http.Handler) http.Handler { - // FIXME: there is a strange error log, not sure why it happens yet - // 2026/03/28 19:50:13 modules/log/misc.go:72:(*loggerToWriter).Write() [I] Unsolicited response received on idle HTTP channel starting with "HTTP/1.1 400 Bad Request\r\n\r\n"; err= return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { if !isViteDevRequest(req) { next.ServeHTTP(resp, req) From 6db45232e8c168140945a2dca1b5cd5713825c25 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sun, 29 Mar 2026 01:53:37 +0800 Subject: [PATCH 095/102] add debug header and error response for vite proxy --- modules/public/vitedev.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/modules/public/vitedev.go b/modules/public/vitedev.go index c1501e2228f50..ddcfd804a773a 100644 --- a/modules/public/vitedev.go +++ b/modules/public/vitedev.go @@ -57,6 +57,16 @@ func getViteDevProxy() *httputil.ReverseProxy { r.SetURL(target) r.Out.Host = target.Host }, + ModifyResponse: func(resp *http.Response) error { + // add a header to indicate the Vite dev server port, + // make developers know that this request is proxied to Vite dev server and which port it is + resp.Header.Add("X-Gitea-Vite-Port", port) + return nil + }, + ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) { + log.Error("Error proxying to Vite dev server: %v", err) + http.Error(w, "Error proxying to Vite dev server: "+err.Error(), http.StatusBadGateway) + }, } viteDevProxy.Store(proxy) return proxy From 147197c89a667e589ec62034847d74af18900dd6 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sun, 29 Mar 2026 01:56:36 +0800 Subject: [PATCH 096/102] tell developers to run make watch-frontend --- templates/base/footer.tmpl | 2 +- templates/base/head_script.tmpl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/base/footer.tmpl b/templates/base/footer.tmpl index e9b4b2effe9d0..33f670a590efe 100644 --- a/templates/base/footer.tmpl +++ b/templates/base/footer.tmpl @@ -9,7 +9,7 @@ {{template "custom/body_outer_post" .}} {{template "base/footer_content" .}} - + {{template "custom/footer" .}} diff --git a/templates/base/head_script.tmpl b/templates/base/head_script.tmpl index feff80ccaaf2f..9654404be6128 100644 --- a/templates/base/head_script.tmpl +++ b/templates/base/head_script.tmpl @@ -31,4 +31,4 @@ If you introduce mistakes in it, Gitea JavaScript code wouldn't run correctly. {{/* in case some pages don't render the pageData, we make sure it is an object to prevent null access */}} window.config.pageData = window.config.pageData || {}; - + From 7ab77f0c6de5f500162b19abe015b9f1684f23d9 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sun, 29 Mar 2026 12:45:02 +0800 Subject: [PATCH 097/102] unify script importing --- modules/public/vitedev.go | 1 + modules/templates/helper.go | 25 ++++++++++++++++++++++++- templates/base/footer.tmpl | 2 +- templates/base/head_script.tmpl | 2 +- 4 files changed, 27 insertions(+), 3 deletions(-) diff --git a/modules/public/vitedev.go b/modules/public/vitedev.go index ddcfd804a773a..3e5bda0ca344f 100644 --- a/modules/public/vitedev.go +++ b/modules/public/vitedev.go @@ -75,6 +75,7 @@ func getViteDevProxy() *httputil.ReverseProxy { // ViteDevMiddleware proxies matching requests to the Vite dev server. // It is registered as middleware in non-production mode and lazily discovers // the Vite dev server port from the port file written by the viteDevServerPortPlugin. +// It is needed because there are container-based development, only Gitea web server's port is exposed. func ViteDevMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { if !isViteDevRequest(req) { diff --git a/modules/templates/helper.go b/modules/templates/helper.go index 1878565038b71..e2de36cbea121 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -6,6 +6,7 @@ package templates import ( "fmt" + "html" "html/template" "net/url" "strconv" @@ -69,6 +70,8 @@ func NewFuncMap() template.FuncMap { return strconv.FormatInt(time.Since(startTime).Nanoseconds()/1e6, 10) + "ms" }, + "AssetURI": public.AssetURI, + "ScriptImport": scriptImport, // ----------------------------------------------------------------- // setting "AppName": func() string { @@ -93,7 +96,6 @@ func NewFuncMap() template.FuncMap { "AppDomain": func() string { // documented in mail-templates.md return setting.Domain }, - "AssetURI": public.AssetURI, "ShowFooterTemplateLoadTime": func() bool { return setting.Other.ShowFooterTemplateLoadTime }, @@ -302,3 +304,24 @@ func QueryBuild(a ...any) template.URL { } return template.URL(s) } + +func scriptImport(attrs ...string) template.HTML { + var sb strings.Builder + sb.WriteString("`) + return template.HTML(sb.String()) +} diff --git a/templates/base/footer.tmpl b/templates/base/footer.tmpl index 33f670a590efe..102b6ae72388d 100644 --- a/templates/base/footer.tmpl +++ b/templates/base/footer.tmpl @@ -9,7 +9,7 @@ {{template "custom/body_outer_post" .}} {{template "base/footer_content" .}} - + {{ScriptImport "type" "module" "src" (AssetURI "js/index.js")}} {{template "custom/footer" .}} diff --git a/templates/base/head_script.tmpl b/templates/base/head_script.tmpl index 9654404be6128..07df28641a2c0 100644 --- a/templates/base/head_script.tmpl +++ b/templates/base/head_script.tmpl @@ -31,4 +31,4 @@ If you introduce mistakes in it, Gitea JavaScript code wouldn't run correctly. {{/* in case some pages don't render the pageData, we make sure it is an object to prevent null access */}} window.config.pageData = window.config.pageData || {}; - +{{ScriptImport "src" (AssetURI "js/iife.js")}} From 64b4969e4daa960e98a79586619ebb24c471b351 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sun, 29 Mar 2026 13:00:01 +0800 Subject: [PATCH 098/102] security --- modules/public/vitedev.go | 26 +++++++++++++++----------- vite.config.ts | 23 +++++++++++++++++------ 2 files changed, 32 insertions(+), 17 deletions(-) diff --git a/modules/public/vitedev.go b/modules/public/vitedev.go index 3e5bda0ca344f..9c8da951fc159 100644 --- a/modules/public/vitedev.go +++ b/modules/public/vitedev.go @@ -123,7 +123,7 @@ func viteDevSourceURL(name string) string { return setting.AppSubURL + "/web_src/js/features/eventsource.sharedworker.ts" } if name == "js/iife.js" { - return setting.AppSubURL + "/__vite_iife.js" + return setting.AppSubURL + "/web_src/js/__vite_iife.js" } if name == "js/index.js" { return setting.AppSubURL + "/web_src/js/index.ts" @@ -139,12 +139,18 @@ func isViteDevRequest(req *http.Request) bool { return wsProtocol == "vite-hmr" || wsProtocol == "vite-ping" } path := req.URL.Path - if strings.HasPrefix(path, "/@vite/") || // HMR client - strings.HasPrefix(path, "/@fs/") || // out-of-root file access - strings.HasPrefix(path, "/@id/") || // virtual modules - strings.HasPrefix(path, "/__vite") || // ping endpoint, iife - strings.HasPrefix(path, "/node_modules/") || // optimized deps - strings.HasPrefix(path, "/web_src/") { // source files + + // vite internal requests + if strings.HasPrefix(path, "/@vite/") /* HMR client */ || + strings.HasPrefix(path, "/@fs/") /* out-of-root file access, see vite.config.ts: fs.allow */ || + strings.HasPrefix(path, "/@id/") /* virtual modules */ { + return true + } + + // local source requests (VITE-DEV-SERVER-SECURITY: don't serve sensitive files outside the allowed paths) + if strings.HasPrefix(path, "/node_modules/") || + strings.HasPrefix(path, "/public/assets/") || + strings.HasPrefix(path, "/web_src/") { return true } @@ -155,10 +161,8 @@ func isViteDevRequest(req *http.Request) bool { // - "{AssetFS}" is a layered filesystem from "{RepoRoot}/public" or embedded assets, and user's custom files in "{CustomPath}/public" // - "{RepoRoot}/assets/emoji.json" just happens to have the dir name "assets", it is not related to frontend assets // - BAD DESIGN: indeed it is a "conflicted and polluted name" sample - if path == "/assets/emoji.json" || strings.HasPrefix(path, "/public/assets/") { - if _, ok := req.URL.Query()["import"]; ok { - return true - } + if path == "/assets/emoji.json" { + return true } return false } diff --git a/vite.config.ts b/vite.config.ts index 360b64296aa5a..d2c7abac054c6 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -130,20 +130,19 @@ function iifePlugin(): Plugin { }); server.middlewares.use((req, res, next) => { + // "__vite_iife" is a virtual file in memory, serve it directly const pathname = req.url!.split('?')[0]; - if (pathname === '/__vite_iife.js') { + if (pathname === '/web_src/js/__vite_iife.js') { res.setHeader('Content-Type', 'application/javascript'); res.setHeader('Cache-Control', 'no-store'); res.end(iifeCode); - return; - } - if (pathname === '/__vite_iife.js.map') { + } else if (pathname === '/web_src/js/__vite_iife.js.map') { res.setHeader('Content-Type', 'application/json'); res.setHeader('Cache-Control', 'no-store'); res.end(iifeMap); - return; + } else { + next(); } - next(); }); }, async closeBundle() { @@ -214,6 +213,18 @@ export default defineConfig(commonViteOpts({ open: false, host: '0.0.0.0', strictPort: false, + fs: { + // VITE-DEV-SERVER-SECURITY: the dev server will be exposed to public by Gitea's web server, so we need to strictly limit the access + // Otherwise `/@fs/*` will be able to access any file (including app.ini which contains INTERNAL_TOKEN) + strict: true, + allow: [ + 'assets', + 'node_modules', + 'public', + 'web_src', + // do not add any other directories here, unless you are absolutely sure it's safe to expose them to the public + ], + }, headers: { 'Cache-Control': 'no-store', // prevent browser disk cache }, From 690ca24945541786bdd2ee6610a914534a67a6a1 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sun, 29 Mar 2026 13:31:24 +0800 Subject: [PATCH 099/102] fix lint --- modules/templates/helper.go | 38 +++++++++++++++++++-------------- templates/base/footer.tmpl | 2 +- templates/base/head_script.tmpl | 2 +- 3 files changed, 24 insertions(+), 18 deletions(-) diff --git a/modules/templates/helper.go b/modules/templates/helper.go index e2de36cbea121..7f47857cd7487 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -11,6 +11,7 @@ import ( "net/url" "strconv" "strings" + "sync" "time" "code.gitea.io/gitea/modules/base" @@ -305,23 +306,28 @@ func QueryBuild(a ...any) template.URL { return template.URL(s) } -func scriptImport(attrs ...string) template.HTML { - var sb strings.Builder - sb.WriteString("`) - return template.HTML(sb.String()) + // the message will be directly put in the onerror JS code's string + onScriptErrorPrompt := `Please make sure the asset files can be accessed.` + if !setting.IsProd { + onScriptErrorPrompt += `\n\nFor development, run: make watch-frontend.` + } + onScriptErrorJS := fmt.Sprintf(`alert('Failed to load asset file from ' + this.src + '. %s')`, onScriptErrorPrompt) + ret.scriptImportRemainingPart = `onerror="` + html.EscapeString(onScriptErrorJS) + `">` + return ret +}) + +func scriptImport(path string, typ ...string) template.HTML { + if len(typ) > 0 { + if typ[0] == "module" { + return template.HTML(` -{{ScriptImport "src" (AssetURI "js/iife.js")}} +{{ScriptImport "js/iife.js"}} From f8f9fb142e163d18db51c428545cf9152ad4b039 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Sun, 29 Mar 2026 13:33:23 +0800 Subject: [PATCH 100/102] fix lint --- modules/templates/helper.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/templates/helper.go b/modules/templates/helper.go index 7f47857cd7487..3a5eb5904f7b0 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -308,7 +308,8 @@ func QueryBuild(a ...any) template.URL { var globalVars = sync.OnceValue(func() (ret struct { scriptImportRemainingPart string -}) { +}, +) { // add onerror handler to alert users when the script fails to load: // * for end users: there were many users reporting that "UI doesn't work", actually they made mistakes in their config // * for developers: help them to remember to run "make watch-frontend" to build frontend assets From 214ab0c4c0b4a350973552ce1bdd408031c7a48f Mon Sep 17 00:00:00 2001 From: silverwind Date: Sun, 29 Mar 2026 10:57:35 +0200 Subject: [PATCH 101/102] Remove Gitpod integration Gitpod has shut down, remove the configuration file, badge links from all READMEs, labeler entry, and devcontainer comment reference. Co-Authored-By: Claude (Opus 4.6) --- .devcontainer/devcontainer.json | 2 +- .github/labeler.yml | 1 - .gitpod.yml | 51 --------------------------------- README.md | 1 - README.zh-cn.md | 1 - README.zh-tw.md | 1 - 6 files changed, 1 insertion(+), 56 deletions(-) delete mode 100644 .gitpod.yml diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 4f82a5d8c6a72..b11c74bef62ca 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -20,7 +20,7 @@ "customizations": { "vscode": { "settings": {}, - // same extensions as Gitpod, should match /.gitpod.yml + // keep in sync with the project's recommended extensions "extensions": [ "editorconfig.editorconfig", "dbaeumer.vscode-eslint", diff --git a/.github/labeler.yml b/.github/labeler.yml index 68a0f30fd64c4..0f3c508041138 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -43,7 +43,6 @@ modifies/internal: - ".editorconfig" - ".eslintrc.cjs" - ".golangci.yml" - - ".gitpod.yml" - ".markdownlint.yaml" - ".spectral.yaml" - "stylelint.config.*" diff --git a/.gitpod.yml b/.gitpod.yml deleted file mode 100644 index 8671edc47cc74..0000000000000 --- a/.gitpod.yml +++ /dev/null @@ -1,51 +0,0 @@ -tasks: - - name: Setup - init: | - cp -r contrib/ide/vscode .vscode - make deps - make build - command: | - gp sync-done setup - exit 0 - - name: Run backend - command: | - gp sync-await setup - - # Get the URL and extract the domain - url=$(gp url 3000) - domain=$(echo $url | awk -F[/:] '{print $4}') - - if [ -f custom/conf/app.ini ]; then - sed -i "s|^ROOT_URL =.*|ROOT_URL = ${url}/|" custom/conf/app.ini - sed -i "s|^DOMAIN =.*|DOMAIN = ${domain}|" custom/conf/app.ini - sed -i "s|^SSH_DOMAIN =.*|SSH_DOMAIN = ${domain}|" custom/conf/app.ini - sed -i "s|^NO_REPLY_ADDRESS =.*|SSH_DOMAIN = noreply.${domain}|" custom/conf/app.ini - else - mkdir -p custom/conf/ - echo -e "[server]\nROOT_URL = ${url}/" > custom/conf/app.ini - echo -e "\n[database]\nDB_TYPE = sqlite3\nPATH = $GITPOD_REPO_ROOT/data/gitea.db" >> custom/conf/app.ini - fi - export TAGS="sqlite sqlite_unlock_notify" - make watch-backend - - name: Run frontend - command: | - gp sync-await setup - make watch-frontend - openMode: split-right - -vscode: - extensions: - - editorconfig.editorconfig - - dbaeumer.vscode-eslint - - golang.go - - stylelint.vscode-stylelint - - DavidAnson.vscode-markdownlint - - Vue.volar - - ms-azuretools.vscode-docker - - vitest.explorer - - cweijan.vscode-database-client2 - - GitHub.vscode-pull-request-github - -ports: - - name: Gitea - port: 3000 diff --git a/README.md b/README.md index ed000971a7555..7ebeac97be057 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,6 @@ [![](https://www.codetriage.com/go-gitea/gitea/badges/users.svg)](https://www.codetriage.com/go-gitea/gitea "Help Contribute to Open Source") [![](https://opencollective.com/gitea/tiers/backers/badge.svg?label=backers&color=brightgreen)](https://opencollective.com/gitea "Become a backer/sponsor of gitea") [![](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT "License: MIT") -[![Contribute with Gitpod](https://img.shields.io/badge/Contribute%20with-Gitpod-908a85?logo=gitpod&color=green)](https://gitpod.io/#https://github.com/go-gitea/gitea) [![](https://badges.crowdin.net/gitea/localized.svg)](https://translate.gitea.com "Crowdin") [繁體中文](./README.zh-tw.md) | [简体中文](./README.zh-cn.md) diff --git a/README.zh-cn.md b/README.zh-cn.md index 8d9531e8e4e24..8ccacc0fea491 100644 --- a/README.zh-cn.md +++ b/README.zh-cn.md @@ -8,7 +8,6 @@ [![](https://www.codetriage.com/go-gitea/gitea/badges/users.svg)](https://www.codetriage.com/go-gitea/gitea "Help Contribute to Open Source") [![](https://opencollective.com/gitea/tiers/backers/badge.svg?label=backers&color=brightgreen)](https://opencollective.com/gitea "Become a backer/sponsor of gitea") [![](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT "License: MIT") -[![Contribute with Gitpod](https://img.shields.io/badge/Contribute%20with-Gitpod-908a85?logo=gitpod&color=green)](https://gitpod.io/#https://github.com/go-gitea/gitea) [![](https://badges.crowdin.net/gitea/localized.svg)](https://translate.gitea.com "Crowdin") [English](./README.md) | [繁體中文](./README.zh-tw.md) diff --git a/README.zh-tw.md b/README.zh-tw.md index 875d31e28a856..4160fd0bd91c2 100644 --- a/README.zh-tw.md +++ b/README.zh-tw.md @@ -8,7 +8,6 @@ [![](https://www.codetriage.com/go-gitea/gitea/badges/users.svg)](https://www.codetriage.com/go-gitea/gitea "Help Contribute to Open Source") [![](https://opencollective.com/gitea/tiers/backers/badge.svg?label=backers&color=brightgreen)](https://opencollective.com/gitea "Become a backer/sponsor of gitea") [![](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT "License: MIT") -[![Contribute with Gitpod](https://img.shields.io/badge/Contribute%20with-Gitpod-908a85?logo=gitpod&color=green)](https://gitpod.io/#https://github.com/go-gitea/gitea) [![](https://badges.crowdin.net/gitea/localized.svg)](https://translate.gitea.com "Crowdin") [English](./README.md) | [简体中文](./README.zh-cn.md) From f2d119aceb85f5f54f805a5e31a51036d0aac21d Mon Sep 17 00:00:00 2001 From: silverwind Date: Sun, 29 Mar 2026 11:00:01 +0200 Subject: [PATCH 102/102] Apply suggestion from @silverwind Signed-off-by: silverwind --- .devcontainer/devcontainer.json | 1 - 1 file changed, 1 deletion(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index b11c74bef62ca..f8e5972af1329 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -20,7 +20,6 @@ "customizations": { "vscode": { "settings": {}, - // keep in sync with the project's recommended extensions "extensions": [ "editorconfig.editorconfig", "dbaeumer.vscode-eslint",