frontend_nyla/vite.config.ts
2026-04-08 22:29:35 +02:00

155 lines
5.2 KiB
TypeScript

import { defineConfig, loadEnv, Plugin } from 'vite';
import react from '@vitejs/plugin-react';
import { createHtmlPlugin } from 'vite-plugin-html';
import type { IncomingMessage, ServerResponse } from 'http';
import * as fs from 'fs';
import * as path from 'path';
type I18nEntry = { context: string; key: string; value: string };
/** Find the nearest enclosing function/component name above a given character offset. */
function _findEnclosingComponent(content: string, charOffset: number): string {
const reFunc = /(?:export\s+)?(?:const|function)\s+([A-Z_]\w*)/g;
let best = '';
let m: RegExpExecArray | null;
while ((m = reFunc.exec(content)) !== null) {
if (m.index > charOffset) break;
best = m[1];
}
return best;
}
/** Scan all .ts/.tsx under srcRoot for t() calls and return structured entries with context. */
function _scanTKeys(srcRoot: string): I18nEntry[] {
const keyMap = new Map<string, Set<string>>();
const reSingle = /\bt\(\s*'((?:\\.|[^'])+)'\s*(?:,|\))/g;
const reDouble = /\bt\(\s*"((?:\\.|[^"])+)"\s*(?:,|\))/g;
const _addKey = (key: string, relFile: string, content: string, offset: number): void => {
const comp = _findEnclosingComponent(content, offset);
const ctx = comp ? `${relFile} > ${comp}` : relFile;
if (!keyMap.has(key)) keyMap.set(key, new Set());
keyMap.get(key)!.add(ctx);
};
const walk = (dir: string): void => {
if (!fs.existsSync(dir)) return;
for (const ent of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, ent.name);
if (ent.isDirectory()) {
walk(full);
} else if (/\.(tsx?)$/i.test(ent.name)) {
const content = fs.readFileSync(full, 'utf-8');
const relFile = path.relative(srcRoot, full).replace(/\\/g, '/');
let m: RegExpExecArray | null;
reSingle.lastIndex = 0;
while ((m = reSingle.exec(content)) !== null) {
const raw = m[1].replace(/\\'/g, "'").replace(/\\\\/g, '\\');
if (raw) _addKey(raw, relFile, content, m.index);
}
reDouble.lastIndex = 0;
while ((m = reDouble.exec(content)) !== null) {
const raw = m[1].replace(/\\"/g, '"').replace(/\\\\/g, '\\');
if (raw) _addKey(raw, relFile, content, m.index);
}
}
}
};
walk(srcRoot);
const entries: I18nEntry[] = [];
for (const [key, contexts] of [...keyMap.entries()].sort((a, b) => a[0].localeCompare(b[0], 'de'))) {
entries.push({ context: 'ui', key, value: [...contexts].sort().join(' | ') });
}
return entries;
}
function extractI18nKeys(): Plugin {
return {
name: 'extract-i18n-keys',
configureServer(server) {
server.middlewares.use((req: IncomingMessage, res: ServerResponse, next: () => void) => {
const url = req.url?.split('?')[0] ?? '';
if (!url.endsWith('/i18n-keys.json')) {
next();
return;
}
const srcDir = path.join(process.cwd(), 'src');
const payload = JSON.stringify(_scanTKeys(srcDir), null, 2);
res.setHeader('Content-Type', 'application/json; charset=utf-8');
res.end(payload);
});
},
writeBundle(options) {
const outDir = options.dir ?? path.resolve(process.cwd(), 'dist');
const srcDir = path.join(process.cwd(), 'src');
const entries = _scanTKeys(srcDir);
if (!fs.existsSync(outDir)) {
fs.mkdirSync(outDir, { recursive: true });
}
fs.writeFileSync(path.join(outDir, 'i18n-keys.json'), `${JSON.stringify(entries)}\n`, 'utf-8');
},
};
}
// Custom plugin to serve static HTML files from public directory BEFORE SPA fallback
function serveStaticHtml(): Plugin {
return {
name: 'serve-static-html',
enforce: 'pre',
configureServer(server) {
// Directly adding middleware runs BEFORE internal Vite middlewares
server.middlewares.use((req, res, next) => {
// Check if request is for a static HTML file in public
const url = req.url?.split('?')[0]; // Remove query string
if (url && url.endsWith('.html') && url !== '/' && url !== '/index.html') {
const filePath = path.join(process.cwd(), 'public', url);
if (fs.existsSync(filePath)) {
res.setHeader('Content-Type', 'text/html; charset=utf-8');
res.statusCode = 200;
res.end(fs.readFileSync(filePath, 'utf-8'));
return;
}
}
next();
});
},
};
}
export default defineConfig(({ mode }) => {
// Load env file based on mode
const env = loadEnv(mode, process.cwd(), '');
return {
plugins: [
// Serve static HTML files
serveStaticHtml(),
extractI18nKeys(),
react(),
createHtmlPlugin({
// Only process main index.html, not public static files
pages: [
{
entry: 'src/main.tsx',
filename: 'index.html',
template: 'index.html',
injectOptions: {
data: {
VITE_APP_NAME: env.VITE_APP_NAME || 'PowerOn',
},
},
},
],
}),
],
envPrefix: 'VITE_',
css: {
modules: {
scopeBehaviour: 'local', // Default behavior for CSS modules
},
},
// Ensure public files are served correctly as static
publicDir: 'public',
};
});