ARTEX — AI 自主渗透测试系统(Go 后端 + Next.js 前端,单二进制内嵌)。 Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
132 lines
4.4 KiB
TypeScript
132 lines
4.4 KiB
TypeScript
/**
|
||
* Script: generate-theme-presets.ts
|
||
*
|
||
* This script scans the /styles/presets directory for CSS files containing theme definitions.
|
||
* It extracts `label:`, `value:`, and primary color definitions (`--primary`) for both light and dark modes.
|
||
* These primary colors are used to visually represent each theme in the UI (e.g., colored dots or theme previews).
|
||
* Default theme colors are fetched from /app/globals.css.
|
||
* All extracted metadata is injected into a marked section of the /lib/preferences/theme.ts file.
|
||
*
|
||
* Usage:
|
||
* - During local development, run manually after adding any new theme preset:
|
||
* npm run generate:presets
|
||
* - Ensure that each new CSS preset includes `label:` and `value:` comments.
|
||
* - This generation step is currently automated using a Husky pre-push hook.
|
||
* - You may optionally integrate it directly into a build step if preferred.
|
||
*/
|
||
|
||
import { execFileSync } from "node:child_process";
|
||
import fs from "node:fs";
|
||
import path from "node:path";
|
||
|
||
const presetDir = path.resolve(__dirname, "../styles/presets");
|
||
|
||
if (!fs.existsSync(presetDir)) {
|
||
console.error(`❌ Preset directory not found at: ${presetDir}`);
|
||
process.exit(1);
|
||
}
|
||
|
||
const outputPath = path.resolve(__dirname, "../lib/preferences/theme.ts");
|
||
|
||
const files = fs.readdirSync(presetDir).filter((file) => file.endsWith(".css"));
|
||
|
||
if (files.length === 0) {
|
||
console.warn("⚠️ No preset CSS files found. Only default preset will be included.");
|
||
}
|
||
|
||
const presets = files.map((file) => {
|
||
const filePath = path.join(presetDir, file);
|
||
const content = fs.readFileSync(filePath, "utf8");
|
||
|
||
const labelMatch = content.match(/label:\s*(.+)/);
|
||
const valueMatch = content.match(/value:\s*(.+)/);
|
||
|
||
if (!labelMatch) {
|
||
console.warn(`⚠️ No 'label:' found in ${file}, using filename as fallback.`);
|
||
}
|
||
if (!valueMatch) {
|
||
console.warn(`⚠️ No 'value:' found in ${file}, using filename as fallback.`);
|
||
}
|
||
|
||
const label = labelMatch?.[1]?.trim() ?? file.replace(".css", "");
|
||
const value = valueMatch?.[1]?.trim() ?? file.replace(".css", "");
|
||
|
||
const lightPrimaryMatch = content.match(/:root\[data-theme-preset="[^"]*"\][\s\S]*?--primary:\s*([^;]+);/);
|
||
const darkPrimaryMatch = content.match(/\.dark:root\[data-theme-preset="[^"]*"\][\s\S]*?--primary:\s*([^;]+);/);
|
||
|
||
const primary = {
|
||
light: lightPrimaryMatch?.[1]?.trim() ?? "",
|
||
dark: darkPrimaryMatch?.[1]?.trim() ?? "",
|
||
};
|
||
|
||
if (!lightPrimaryMatch || !darkPrimaryMatch) {
|
||
console.warn(`⚠️ Missing --primary for ${file} (light or dark). Check CSS syntax.`);
|
||
}
|
||
|
||
return { label, value, primary };
|
||
});
|
||
|
||
const globalStylesPath = path.resolve(__dirname, "../app/globals.css");
|
||
|
||
let globalContent = "";
|
||
try {
|
||
globalContent = fs.readFileSync(globalStylesPath, "utf8");
|
||
} catch (err) {
|
||
console.error(`❌ Could not read globals.css at ${globalStylesPath}`);
|
||
console.error(err);
|
||
process.exit(1);
|
||
}
|
||
|
||
const defaultLightPrimaryRegex = /:root\s*{[^}]*--primary:\s*([^;]+);/;
|
||
const defaultDarkPrimaryRegex = /\.dark\s*{[^}]*--primary:\s*([^;]+);/;
|
||
|
||
const defaultLightPrimaryMatch = defaultLightPrimaryRegex.exec(globalContent);
|
||
const defaultDarkPrimaryMatch = defaultDarkPrimaryRegex.exec(globalContent);
|
||
|
||
const defaultPrimary = {
|
||
light: defaultLightPrimaryMatch?.[1]?.trim() ?? "",
|
||
dark: defaultDarkPrimaryMatch?.[1]?.trim() ?? "",
|
||
};
|
||
|
||
presets.unshift({ label: "Default", value: "default", primary: defaultPrimary });
|
||
|
||
const generatedBlock = `// --- generated:themePresets:start ---
|
||
|
||
export const THEME_PRESET_OPTIONS = ${JSON.stringify(presets, null, 2)} as const;
|
||
|
||
export const THEME_PRESET_VALUES = THEME_PRESET_OPTIONS.map((p) => p.value);
|
||
|
||
export type ThemePreset = (typeof THEME_PRESET_OPTIONS)[number]["value"];
|
||
|
||
// --- generated:themePresets:end ---`;
|
||
|
||
const fileContent = fs.readFileSync(outputPath, "utf8");
|
||
|
||
const updated = fileContent.replace(
|
||
/\/\/ --- generated:themePresets:start ---[\s\S]*?\/\/ --- generated:themePresets:end ---/,
|
||
generatedBlock,
|
||
);
|
||
|
||
function main() {
|
||
const biomeBin = require.resolve("@biomejs/biome/bin/biome");
|
||
const formatted = execFileSync(process.execPath, [biomeBin, "format", "--stdin-file-path", outputPath], {
|
||
input: updated,
|
||
encoding: "utf8",
|
||
});
|
||
|
||
if (formatted === fileContent) {
|
||
console.log("ℹ️ No changes in theme.ts");
|
||
return;
|
||
}
|
||
|
||
fs.writeFileSync(outputPath, formatted);
|
||
console.log("✅ theme.ts updated with new theme presets");
|
||
}
|
||
|
||
try {
|
||
main();
|
||
} catch (err) {
|
||
console.error("❌ Unexpected error while generating theme presets:", err);
|
||
process.exit(1);
|
||
}
|