表格布局替换为按分组展示的卡片式布局,新增 group 字段配置和 TargetBoard/TargetCard 等组件。模态框详情页支持时间范围筛选和分页,SummaryCards 减为 3 个。API 端点变更:trend/history 改用 from/to 参数,history 支持分页。recentSampleCount 硬编码为 30。
142 lines
4.4 KiB
TypeScript
142 lines
4.4 KiB
TypeScript
import { mkdir, readdir, rm, writeFile } from "node:fs/promises";
|
|
import { dirname, relative, sep } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { $ } from "bun";
|
|
|
|
const buildDir = fileURLToPath(new URL("../.build/", import.meta.url));
|
|
const webDistDir = fileURLToPath(new URL("../dist/web/", import.meta.url));
|
|
const executablePath = fileURLToPath(new URL("../dist/gateway-checker", import.meta.url));
|
|
const generatedAssetsPath = fileURLToPath(new URL("../.build/static-assets.ts", import.meta.url));
|
|
const generatedEntryPath = fileURLToPath(new URL("../.build/server-entry.ts", import.meta.url));
|
|
|
|
await rm(buildDir, { recursive: true, force: true });
|
|
await rm(executablePath, { force: true });
|
|
await mkdir(buildDir, { recursive: true });
|
|
|
|
await $`bunx --bun vite build`;
|
|
|
|
const files = await listFiles(webDistDir);
|
|
const indexPath = files.find((file) => normalize(relative(webDistDir, file)) === "index.html");
|
|
|
|
if (!indexPath) {
|
|
throw new Error("Vite build 未生成 dist/web/index.html");
|
|
}
|
|
|
|
const assetFiles = files.filter((file) => file !== indexPath);
|
|
await writeGeneratedAssets(indexPath, assetFiles);
|
|
await writeGeneratedEntry();
|
|
|
|
const target = process.env.BUN_TARGET ?? process.env.BUILD_TARGET;
|
|
const result = await Bun.build({
|
|
entrypoints: [generatedEntryPath],
|
|
compile: target
|
|
? {
|
|
target: target as Bun.Build.CompileTarget,
|
|
outfile: executablePath,
|
|
autoloadDotenv: true,
|
|
autoloadBunfig: true,
|
|
}
|
|
: {
|
|
outfile: executablePath,
|
|
autoloadDotenv: true,
|
|
autoloadBunfig: true,
|
|
},
|
|
minify: true,
|
|
sourcemap: "linked",
|
|
});
|
|
|
|
if (!result.success) {
|
|
await rm(executablePath, { force: true });
|
|
throw new Error("Bun executable 构建失败");
|
|
}
|
|
|
|
console.log(`Built executable: ${executablePath}`);
|
|
|
|
await rm(buildDir, { recursive: true, force: true });
|
|
|
|
async function listFiles(directory: string): Promise<string[]> {
|
|
const entries = await readdir(directory, { withFileTypes: true });
|
|
const files = await Promise.all(
|
|
entries.map(async (entry) => {
|
|
const path = `${directory.replace(/\/$/, "")}/${entry.name}`;
|
|
|
|
if (entry.isDirectory()) {
|
|
return listFiles(path);
|
|
}
|
|
|
|
return [path];
|
|
}),
|
|
);
|
|
|
|
return files.flat().sort((left, right) => normalize(left).localeCompare(normalize(right)));
|
|
}
|
|
|
|
async function writeGeneratedAssets(indexPath: string, assetFiles: string[]) {
|
|
const imports = [
|
|
`import type { StaticAssets } from "../src/server/app";`,
|
|
`import indexPath from "${toImportPath(indexPath)}" with { type: "file" };`,
|
|
...assetFiles.map((file, index) => `import asset${index}Path from "${toImportPath(file)}" with { type: "file" };`),
|
|
];
|
|
const assetEntries = assetFiles.map((file, index) => {
|
|
const urlPath = `/${normalize(relative(webDistDir, file))}`;
|
|
return ` ${JSON.stringify(urlPath)}: Bun.file(asset${index}Path),`;
|
|
});
|
|
const source = `${imports.join("\n")}
|
|
|
|
export const staticAssets: StaticAssets = {
|
|
indexHtml: Bun.file(indexPath),
|
|
files: {
|
|
${assetEntries.join("\n")}
|
|
},
|
|
};
|
|
`;
|
|
|
|
await mkdir(dirname(generatedAssetsPath), { recursive: true });
|
|
await writeFile(generatedAssetsPath, source);
|
|
}
|
|
|
|
async function writeGeneratedEntry() {
|
|
await writeFile(
|
|
generatedEntryPath,
|
|
`import { loadConfig } from "../src/server/checker/config-loader";
|
|
import { ProbeStore } from "../src/server/checker/store";
|
|
import { ProbeEngine } from "../src/server/checker/engine";
|
|
import { startServer } from "../src/server/server";
|
|
import { readRuntimeConfig } from "../src/server/config";
|
|
import { staticAssets } from "./static-assets";
|
|
|
|
async function main() {
|
|
const { configPath } = readRuntimeConfig();
|
|
const config = await loadConfig(configPath);
|
|
|
|
const store = new ProbeStore(config.dataDir + "/probe.db");
|
|
store.syncTargets(config.targets);
|
|
|
|
const engine = new ProbeEngine(store, config.targets, config.maxConcurrentChecks);
|
|
engine.start();
|
|
|
|
startServer({
|
|
config: { host: config.host, port: config.port },
|
|
mode: "production",
|
|
staticAssets,
|
|
store,
|
|
});
|
|
}
|
|
|
|
void main().catch((error) => {
|
|
console.error("启动失败:", error instanceof Error ? error.message : error);
|
|
process.exit(1);
|
|
});
|
|
`,
|
|
);
|
|
}
|
|
|
|
function toImportPath(path: string): string {
|
|
const rel = normalize(relative(buildDir, path));
|
|
return rel.startsWith(".") ? rel : `./${rel}`;
|
|
}
|
|
|
|
function normalize(path: string): string {
|
|
return path.split(sep).join("/");
|
|
}
|