155 lines
5.2 KiB
TypeScript
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',
|
|
};
|
|
});
|