Fix theme discovery and Vite dev server in dev mode (#37033)

1. In dev mode, discover themes from source files in
`web_src/css/themes/` instead of AssetFS. In prod, use AssetFS only.
Extract shared `collectThemeFiles` helper to deduplicate theme file
handling.
2. Implement `fs.ReadDirFS` on `LayeredFS` to support theme file
discovery.
3. `IsViteDevMode` now performs an HTTP health check against the vite
dev server instead of only checking the port file exists. Result is
cached with a 1-second TTL.
4. Refactor theme caching from mutex to atomic pointer with time-based
invalidation, allowing themes to refresh when vite dev mode state
changes.
5. Move `ViteDevMiddleware` into `ProtocolMiddlewares` so it applies to
both install and web routes.
6. Show a `ViteDevMode` label in the page footer when vite dev server is
active.
7. Add `/__vite_dev_server_check` endpoint to vite dev server for the
health check.
8. Ensure `.vite` directory exists before writing the dev-port file.
9. Minor CSS fixes: footer gap, navbar mobile alignment.

---
This PR was written with the help of Claude Opus 4.6

---------

Signed-off-by: silverwind <me@silverwind.io>
Co-authored-by: Claude (Opus 4.6) <noreply@anthropic.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
silverwind
2026-03-30 16:59:10 +02:00
committed by GitHub
parent 539654831a
commit 612ce46cda
10 changed files with 160 additions and 75 deletions

View File

@@ -1,8 +1,8 @@
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 {join, parse} from 'node:path';
import {readFileSync, writeFileSync, mkdirSync, unlinkSync, globSync} from 'node:fs';
import path, {join, parse} from 'node:path';
import {env} from 'node:process';
import tailwindcss from 'tailwindcss';
import tailwindConfig from './tailwind.config.ts';
@@ -132,7 +132,9 @@ 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 === '/web_src/js/__vite_iife.js') {
if (pathname === '/web_src/js/__vite_dev_server_check') {
res.end('ok');
} else if (pathname === '/web_src/js/__vite_iife.js') {
res.setHeader('Content-Type', 'application/javascript');
res.setHeader('Cache-Control', 'no-store');
res.end(iifeCode);
@@ -198,6 +200,7 @@ function viteDevServerPortPlugin(): Plugin {
server.httpServer!.once('listening', () => {
const addr = server.httpServer!.address();
if (typeof addr === 'object' && addr) {
mkdirSync(path.dirname(viteDevPortFilePath), {recursive: true});
writeFileSync(viteDevPortFilePath, String(addr.port));
}
});