Files
ONE-OS/axhub-make/vite-plugins/themesApiPlugin.ts
王冕 a27e3b8e43 feat: sync full workspace including web modules, docs, and configurations to Gitea
Optimized the root .gitignore to exclude virtual environments, node modules,
and temp folders to ensure clean and lightweight version tracking.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-09 18:12:25 +08:00

301 lines
11 KiB
TypeScript

import type { Plugin } from 'vite';
import fs from 'fs';
import path from 'path';
import { getRequestPathname, readJsonBody } from './utils/httpUtils';
export function themesApiPlugin(): Plugin {
return {
name: 'themes-api-plugin',
configureServer(server: any) {
server.middlewares.use((req: any, res: any, next: any) => {
const pathname = getRequestPathname(req);
if (!pathname.startsWith('/api/themes')) {
return next();
}
// Sync a theme's DESIGN.md to the project root
if (req.method === 'POST' && pathname === '/api/themes/sync-design') {
(async () => {
try {
const body = await readJsonBody(req);
const themeName = (body?.themeName || '').trim();
const rootDesignPath = path.resolve(process.cwd(), 'DESIGN.md');
if (!themeName) {
// Clear: remove root DESIGN.md if it exists
if (fs.existsSync(rootDesignPath)) {
fs.unlinkSync(rootDesignPath);
}
res.statusCode = 200;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ success: true, removed: true }));
return;
}
const themesDir = path.resolve(process.cwd(), 'src/themes');
const themeDesignPath = path.join(themesDir, themeName, 'DESIGN.md');
if (!themeDesignPath.startsWith(themesDir)) {
res.statusCode = 403;
res.end(JSON.stringify({ error: 'Forbidden' }));
return;
}
if (!fs.existsSync(themeDesignPath)) {
// Theme has no DESIGN.md — remove root copy if present
if (fs.existsSync(rootDesignPath)) {
fs.unlinkSync(rootDesignPath);
}
res.statusCode = 200;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ success: true, skipped: true }));
return;
}
fs.copyFileSync(themeDesignPath, rootDesignPath);
res.statusCode = 200;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ success: true }));
} catch (error: any) {
console.error('Error syncing DESIGN.md:', error);
res.statusCode = 500;
res.end(JSON.stringify({ error: error.message }));
}
})();
return;
}
if (req.method === 'DELETE' && pathname !== '/api/themes' && pathname !== '/api/themes/') {
try {
const themeName = pathname.replace('/api/themes/', '');
if (!themeName) {
res.statusCode = 400;
res.end(JSON.stringify({ error: 'Missing theme name' }));
return;
}
const themesDir = path.resolve(process.cwd(), 'src/themes');
const themeDir = path.join(themesDir, themeName);
if (!themeDir.startsWith(themesDir)) {
res.statusCode = 403;
res.end(JSON.stringify({ error: 'Forbidden' }));
return;
}
if (!fs.existsSync(themeDir)) {
res.statusCode = 404;
res.end(JSON.stringify({ error: 'Theme not found' }));
return;
}
fs.rmSync(themeDir, { recursive: true, force: true });
res.statusCode = 200;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ success: true }));
} catch (error: any) {
console.error('Error deleting theme:', error);
res.statusCode = 500;
res.end(JSON.stringify({ error: error.message }));
}
return;
}
if (req.method === 'PUT' && pathname !== '/api/themes' && pathname !== '/api/themes/') {
(async () => {
try {
const themeName = pathname.replace('/api/themes/', '');
if (!themeName) {
res.statusCode = 400;
res.end(JSON.stringify({ error: 'Missing theme name' }));
return;
}
const body = await readJsonBody(req);
const displayName = (body?.displayName || '').trim();
if (!displayName) {
res.statusCode = 400;
res.end(JSON.stringify({ error: 'Missing displayName' }));
return;
}
const themesDir = path.resolve(process.cwd(), 'src/themes');
const themeDir = path.join(themesDir, themeName);
if (!themeDir.startsWith(themesDir)) {
res.statusCode = 403;
res.end(JSON.stringify({ error: 'Forbidden' }));
return;
}
if (!fs.existsSync(themeDir)) {
res.statusCode = 404;
res.end(JSON.stringify({ error: 'Theme not found' }));
return;
}
const designTokenPath = path.join(themeDir, 'designToken.json');
let designToken: any = {};
if (fs.existsSync(designTokenPath)) {
try {
designToken = JSON.parse(fs.readFileSync(designTokenPath, 'utf8'));
} catch (error: any) {
res.statusCode = 400;
res.end(JSON.stringify({ error: error.message || 'Invalid designToken.json' }));
return;
}
}
designToken.name = displayName;
fs.writeFileSync(designTokenPath, JSON.stringify(designToken, null, 2));
res.statusCode = 200;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ success: true }));
} catch (error: any) {
console.error('Error updating theme name:', error);
res.statusCode = 500;
res.end(JSON.stringify({ error: error.message }));
}
})();
return;
}
if (req.method !== 'GET') {
return next();
}
if (pathname !== '/api/themes' && pathname !== '/api/themes/') {
try {
const themeName = pathname.replace('/api/themes/', '');
if (!themeName) {
return next();
}
const themesDir = path.resolve(process.cwd(), 'src/themes');
const themeDir = path.join(themesDir, themeName);
if (!themeDir.startsWith(themesDir)) {
res.statusCode = 403;
res.end(JSON.stringify({ error: 'Forbidden' }));
return;
}
if (fs.existsSync(themeDir) && fs.statSync(themeDir).isDirectory()) {
const designTokenPath = path.join(themeDir, 'designToken.json');
const indexHtmlPath = path.join(themeDir, 'index.html');
const themeData: any = { name: themeName, displayName: themeName };
if (fs.existsSync(designTokenPath)) {
try {
const designToken = JSON.parse(fs.readFileSync(designTokenPath, 'utf8'));
themeData.designToken = designToken;
if (designToken && designToken.name) {
themeData.displayName = designToken.name;
}
} catch (error) {
console.error('Error parsing designToken.json:', error);
}
}
if (fs.existsSync(indexHtmlPath)) {
themeData.indexHtml = fs.readFileSync(indexHtmlPath, 'utf8');
}
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify(themeData));
} else {
res.statusCode = 404;
res.end(JSON.stringify({ error: 'Theme not found' }));
}
} catch (error: any) {
console.error('Error loading theme:', error);
res.statusCode = 500;
res.end(JSON.stringify({ error: error.message }));
}
return;
}
try {
const themesDir = path.resolve(process.cwd(), 'src/themes');
const themes: any[] = [];
if (fs.existsSync(themesDir)) {
const items = fs.readdirSync(themesDir, { withFileTypes: true });
items.forEach((item) => {
if (item.isDirectory()) {
const themeDir = path.join(themesDir, item.name);
const designTokenPath = path.join(themeDir, 'designToken.json');
const globalsPath = path.join(themeDir, 'globals.css');
const designSpecPath = path.join(themeDir, 'DESIGN.md');
const indexTsxPath = path.join(themeDir, 'index.tsx');
let displayName = item.name;
const hasDesignToken = fs.existsSync(designTokenPath);
const hasGlobals = fs.existsSync(globalsPath);
const hasDesignSpec = fs.existsSync(designSpecPath);
const hasIndexTsx = fs.existsSync(indexTsxPath);
if (hasDesignToken) {
try {
const designToken = JSON.parse(fs.readFileSync(designTokenPath, 'utf8'));
if (designToken && designToken.name) {
displayName = designToken.name;
}
} catch (error) {
console.error(`Error loading theme ${item.name} designToken:`, error);
}
}
let description = '';
let hasDoc = false;
const readmePath = path.join(themeDir, 'README.md');
if (fs.existsSync(readmePath)) {
try {
const content = fs.readFileSync(readmePath, 'utf8');
const firstLine = content.split('\n')[0];
description = firstLine.replace(/^#\s*/, '').trim();
hasDoc = true;
} catch (error) {
console.warn(`Failed to read README.md for ${item.name}:`, error);
}
} else if (fs.existsSync(designSpecPath)) {
try {
const content = fs.readFileSync(designSpecPath, 'utf8');
const firstLine = content.split('\n')[0];
description = firstLine.replace(/^#\s*/, '').trim();
hasDoc = true;
} catch (error) {
console.warn(`Failed to read DESIGN.md for ${item.name}:`, error);
}
}
themes.push({
name: item.name,
displayName,
description,
hasDoc,
hasDesignToken,
hasGlobals,
hasDesignSpec,
hasIndexTsx,
});
}
});
}
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify(themes));
} catch (error: any) {
console.error('Error loading themes:', error);
res.statusCode = 500;
res.end(JSON.stringify({ error: error.message }));
}
});
},
};
}