1
0

chore: 强化代码质量与风格检查体系

ESLint 升级到 recommended-type-checked + stylistic-type-checked,
引入 perfectionist 导入排序和 import 插件导入验证。

Prettier 显式声明全部格式化参数,消除跨环境差异。
TypeScript 启用 noUnusedLocals 和 noPropertyAccessFromIndexSignature。
完善 ignore 列表,排除 .agents/、bun.lock、data/ 等。
引入 husky + lint-staged(pre-commit)+ commitlint(commit-msg)。
更新 DEVELOPMENT.md 代码质量章节。
修复所有新增规则检测到的类型和风格违规。
This commit is contained in:
2026-05-12 18:44:59 +08:00
parent ce8baae3d1
commit a5cf6065c2
83 changed files with 2654 additions and 1824 deletions

View File

@@ -1,7 +1,7 @@
import { $ } from "bun";
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));
@@ -9,7 +9,7 @@ const executablePath = fileURLToPath(new URL("../dist/dial-server", import.meta.
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(buildDir, { force: true, recursive: true });
await rm(executablePath, { force: true });
await mkdir(buildDir, { recursive: true });
@@ -26,21 +26,21 @@ const assetFiles = files.filter((file) => file !== indexPath);
await writeGeneratedAssets(indexPath, assetFiles);
await writeGeneratedEntry();
const target = process.env.BUN_TARGET ?? process.env.BUILD_TARGET;
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,
autoloadDotenv: true,
outfile: executablePath,
target: target as Bun.Build.CompileTarget,
}
: {
outfile: executablePath,
autoloadDotenv: true,
autoloadBunfig: true,
autoloadDotenv: true,
outfile: executablePath,
},
entrypoints: [generatedEntryPath],
minify: true,
sourcemap: "linked",
});
@@ -52,7 +52,7 @@ if (!result.success) {
console.log(`Built executable: ${executablePath}`);
await rm(buildDir, { recursive: true, force: true });
await rm(buildDir, { force: true, recursive: true });
async function listFiles(directory: string): Promise<string[]> {
const entries = await readdir(directory, { withFileTypes: true });
@@ -71,6 +71,15 @@ async function listFiles(directory: string): Promise<string[]> {
return files.flat().sort((left, right) => normalize(left).localeCompare(normalize(right)));
}
function normalize(path: string): string {
return path.split(sep).join("/");
}
function toImportPath(path: string): string {
const rel = normalize(relative(buildDir, path));
return rel.startsWith(".") ? rel : `./${rel}`;
}
async function writeGeneratedAssets(indexPath: string, assetFiles: string[]) {
const imports = [
`import type { StaticAssets } from "../src/server/app";`,
@@ -133,12 +142,3 @@ void main().catch((error) => {
`,
);
}
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("/");
}

View File

@@ -3,17 +3,17 @@ import { resolve } from "node:path";
const root = resolve(import.meta.dir, "..");
const patterns: Array<{ glob: string; desc: string }> = [
{ glob: ".build/", desc: "Bun 构建缓存" },
{ glob: ".*.bun-build", desc: "Bun 构建临时文件" },
const patterns: Array<{ desc: string; glob: string }> = [
{ desc: "Bun 构建缓存", glob: ".build/" },
{ desc: "Bun 构建临时文件", glob: ".*.bun-build" },
];
for (const { glob, desc } of patterns) {
for (const { desc, glob } of patterns) {
const entries = await Array.fromAsync(new Bun.Glob(glob).scan({ cwd: root, dot: true }));
if (entries.length === 0) continue;
for (const entry of entries) {
const full = resolve(root, entry);
await rm(full, { recursive: true, force: true });
await rm(full, { force: true, recursive: true });
console.log(`已清理 ${desc}: ${entry}`);
}
}

View File

@@ -7,7 +7,7 @@ const configPath = process.argv[2];
const env = {
...process.env,
BACKEND_PORT: process.env.PORT ?? "3000",
BACKEND_PORT: process.env["PORT"] ?? "3000",
};
const children: ChildProcessInfo[] = [
@@ -15,16 +15,16 @@ const children: ChildProcessInfo[] = [
name: "server",
process: Bun.spawn(["bun", "run", "dev:server", ...(configPath ? [configPath] : [])], {
env,
stdout: "inherit",
stderr: "inherit",
stdout: "inherit",
}),
},
{
name: "web",
process: Bun.spawn(["bun", "run", "dev:web"], {
env,
stdout: "inherit",
stderr: "inherit",
stdout: "inherit",
}),
},
];
@@ -46,7 +46,7 @@ process.on("SIGTERM", () => {
});
const firstExit = await Promise.race(
children.map(async (child) => ({ name: child.name, code: await child.process.exited })),
children.map(async (child) => ({ code: await child.process.exited, name: child.name })),
);
stopChildren();

View File

@@ -1,8 +1,9 @@
import { access } from "node:fs/promises";
import { fileURLToPath } from "node:url";
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { access } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import type { HealthResponse, SummaryResponse } from "../src/shared/api";
const executablePath = process.argv[2] ?? fileURLToPath(new URL("../dist/dial-server", import.meta.url));
@@ -12,7 +13,7 @@ await assertExecutableExists(executablePath);
const tempDir = mkdtempSync(join(tmpdir(), "dial-smoke-"));
const configPath = join(tempDir, "probes.yaml");
const port = await getFreePort();
const port = getFreePort();
const baseUrl = `http://127.0.0.1:${port}`;
writeFileSync(
@@ -31,9 +32,9 @@ targets:
`,
);
const app = Bun.spawn([executablePath, configPath], {
stdout: "pipe",
stderr: "pipe",
env: { ...process.env },
stderr: "pipe",
stdout: "pipe",
});
const stdout = readStream(app.stdout);
const stderr = readStream(app.stderr);
@@ -49,10 +50,10 @@ try {
assert(summary.total === 1, "总览统计: total 应为 1");
assertSecurityHeaders(await fetch(`${baseUrl}/api/summary`), "/api/summary");
const { body: targets } = await expectJson(`${baseUrl}/api/targets`, 200);
const { body: targets } = await expectJson<unknown[]>(`${baseUrl}/api/targets`, 200);
assert(Array.isArray(targets), "/api/targets 应返回数组");
assert(targets.length === 1, "/api/targets 应有 1 个目标");
assert(targets[0].name === "httpbin", "目标名称应为 httpbin");
assert((targets[0] as { name: string }).name === "httpbin", "目标名称应为 httpbin");
const missingApi = await fetch(`${baseUrl}/api/not-found`);
assert(missingApi.status === 404, "未知 API 应返回 404");
@@ -67,7 +68,7 @@ try {
const { body: fallbackHtml } = await expectText(`${baseUrl}/dashboard`, 200);
assert(fallbackHtml.includes("DiAL"), "SPA fallback 未返回前端入口页面");
const assetPath = rootHtml.match(/(?:src|href)="(\/assets\/[^"]+)"/)?.[1];
const assetPath = /(?:src|href)="(\/assets\/[^"]+)"/.exec(rootHtml)?.[1];
assert(assetPath !== undefined, "前端入口页面未引用 /assets/* 资源");
const asset = await fetch(`${baseUrl}${assetPath}`);
@@ -85,7 +86,13 @@ try {
throw new Error(`executable smoke test 失败: ${message}\nstdout:\n${out}\nstderr:\n${err}`, { cause: error });
} finally {
app.kill();
rmSync(tempDir, { recursive: true, force: true });
rmSync(tempDir, { force: true, recursive: true });
}
function assert(condition: boolean, message: string): asserts condition {
if (!condition) {
throw new Error(message);
}
}
async function assertExecutableExists(path: string) {
@@ -96,37 +103,12 @@ async function assertExecutableExists(path: string) {
}
}
async function getFreePort(): Promise<number> {
const server = Bun.serve({
hostname: "127.0.0.1",
port: 0,
fetch: () => new Response("ok"),
});
const port = server.port;
server.stop(true);
if (port === undefined) {
throw new Error("无法分配 smoke test 端口");
}
return port;
}
async function waitForServer(url: string) {
const deadline = Date.now() + 8_000;
while (Date.now() < deadline) {
try {
const response = await fetch(url);
if (response.ok) return;
} catch {
await Bun.sleep(100);
}
}
throw new Error(`服务未在超时时间内启动: ${url}`);
function assertSecurityHeaders(response: Response, label: string) {
assert(response.headers.get("x-content-type-options") === "nosniff", `${label} 缺少 nosniff 安全头`);
assert(
response.headers.get("referrer-policy") === "strict-origin-when-cross-origin",
`${label} 缺少 Referrer-Policy 安全头`,
);
}
async function expectJson<T = unknown>(url: string, status: number): Promise<{ body: T; response: Response }> {
@@ -146,22 +128,41 @@ async function expectText(url: string, status: number): Promise<{ body: string;
return { body: await response.text(), response };
}
function assertSecurityHeaders(response: Response, label: string) {
assert(response.headers.get("x-content-type-options") === "nosniff", `${label} 缺少 nosniff 安全头`);
assert(
response.headers.get("referrer-policy") === "strict-origin-when-cross-origin",
`${label} 缺少 Referrer-Policy 安全头`,
);
}
function getFreePort(): number {
const server = Bun.serve({
fetch: () => new Response("ok"),
hostname: "127.0.0.1",
port: 0,
});
const port = server.port;
function assert(condition: boolean, message: string): asserts condition {
if (!condition) {
throw new Error(message);
void server.stop(true);
if (port === undefined) {
throw new Error("无法分配 smoke test 端口");
}
return port;
}
async function readStream(stream: ReadableStream<Uint8Array> | null): Promise<string> {
async function readStream(stream: null | ReadableStream<Uint8Array>): Promise<string> {
if (!stream) return "";
return new Response(stream).text();
}
async function waitForServer(url: string) {
const deadline = Date.now() + 8_000;
while (Date.now() < deadline) {
try {
const response = await fetch(url);
if (response.ok) return;
} catch {
await Bun.sleep(100);
}
}
throw new Error(`服务未在超时时间内启动: ${url}`);
}