1
0

feat: 完善全栈打包质量门禁

在业务开发前补齐 lint、format、verify 与生产运行时契约,确保开发联调和 executable 打包链路可重复验证。
This commit is contained in:
2026-05-09 14:48:49 +08:00
parent 5b412c624d
commit 3f477d1b57
20 changed files with 742 additions and 47 deletions

View File

@@ -3,7 +3,6 @@ import { dirname, relative, sep } from "node:path";
import { fileURLToPath } from "node:url";
import { $ } from "bun";
const rootDir = fileURLToPath(new URL("../", import.meta.url));
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));
@@ -67,16 +66,14 @@ async function listFiles(directory: string): Promise<string[]> {
}),
);
return files.flat();
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" };`,
),
...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))}`;

View File

@@ -5,7 +5,7 @@ interface ChildProcessInfo {
const env = {
...process.env,
BACKEND_PORT: process.env.BACKEND_PORT ?? process.env.PORT ?? "3000",
BACKEND_PORT: process.env.PORT ?? "3000",
};
const children: ChildProcessInfo[] = [

View File

@@ -23,30 +23,41 @@ const stderr = readStream(app.stderr);
try {
await waitForServer(`${baseUrl}/health`);
const health = await expectJson<HealthResponse>(`${baseUrl}/health`, 200);
const { body: health, response: healthResponse } = await expectJson<HealthResponse>(`${baseUrl}/health`, 200);
assert(health.ok === true, "健康检查响应缺少 ok=true");
assertSecurityHeaders(healthResponse, "/health");
const demo = await expectJson<DemoResponse>(`${baseUrl}/api/demo`, 200);
const { body: demo, response: demoResponse } = await expectJson<DemoResponse>(`${baseUrl}/api/demo`, 200);
assert(demo.message.includes("/api/demo"), "demo 响应未包含预期 message");
assert(demo.runtime.mode === "production", "demo 响应 runtime mode 应为 production");
assertSecurityHeaders(demoResponse, "/api/demo");
const missingApi = await fetch(`${baseUrl}/api/not-found`);
assert(missingApi.status === 404, "未知 API 应返回 404");
assert(
missingApi.headers.get("content-type")?.includes("application/json") === true,
"未知 API 应返回 JSON",
);
assert(missingApi.headers.get("content-type")?.includes("application/json") === true, "未知 API 应返回 JSON");
assertSecurityHeaders(missingApi, "/api/not-found");
const rootHtml = await expectText(`${baseUrl}/`, 200);
const { body: rootHtml, response: rootResponse } = await expectText(`${baseUrl}/`, 200);
assert(rootHtml.includes("Gateway Checker Demo"), "前端根页面缺少 demo 标题");
assert(rootResponse.headers.get("cache-control") === "no-cache", "前端根页面应使用 no-cache");
assertSecurityHeaders(rootResponse, "/");
const fallbackHtml = await expectText(`${baseUrl}/dashboard`, 200);
const { body: fallbackHtml, response: fallbackResponse } = await expectText(`${baseUrl}/dashboard`, 200);
assert(fallbackHtml.includes("Gateway Checker Demo"), "SPA fallback 未返回前端入口页面");
assert(fallbackResponse.headers.get("cache-control") === "no-cache", "SPA fallback 应使用 no-cache");
assertSecurityHeaders(fallbackResponse, "/dashboard");
const assetPath = rootHtml.match(/(?:src|href)="(\/assets\/[^"]+)"/)?.[1];
assert(assetPath !== undefined, "前端入口页面未引用 /assets/* 资源");
const asset = await fetch(`${baseUrl}${assetPath}`);
assert(asset.status === 200, `静态资源 ${assetPath} 未返回 200`);
assert(asset.headers.get("cache-control") === "public, max-age=31536000, immutable", "静态资源应使用长缓存");
assertSecurityHeaders(asset, assetPath);
const missingAsset = await expectText(`${baseUrl}/assets/not-found.js`, 404);
assert(!missingAsset.body.includes("Gateway Checker Demo"), "未知静态资源不应返回前端入口页面");
assertSecurityHeaders(missingAsset.response, "/assets/not-found.js");
console.log(`Smoke test passed: ${baseUrl}`);
} catch (error) {
@@ -54,7 +65,7 @@ try {
const [out, err] = await Promise.all([stdout, stderr]);
const message = error instanceof Error ? error.message : String(error);
throw new Error(`executable smoke test 失败: ${message}\nstdout:\n${out}\nstderr:\n${err}`);
throw new Error(`executable smoke test 失败: ${message}\nstdout:\n${out}\nstderr:\n${err}`, { cause: error });
} finally {
app.kill();
}
@@ -62,8 +73,8 @@ try {
async function assertExecutableExists(path: string) {
try {
await access(path);
} catch {
throw new Error(`找不到 executable: ${path},请先运行 bun run build`);
} catch (error) {
throw new Error(`找不到 executable: ${path},请先运行 bun run build`, { cause: error });
}
}
@@ -100,21 +111,29 @@ async function waitForServer(url: string) {
throw new Error(`服务未在超时时间内启动: ${url}`);
}
async function expectJson<T>(url: string, status: number): Promise<T> {
async function expectJson<T>(url: string, status: number): Promise<{ body: T; response: Response }> {
const response = await fetch(url);
assert(response.status === status, `${url} 应返回 ${status},实际为 ${response.status}`);
assert(response.headers.get("content-type")?.includes("application/json") === true, `${url} 应返回 JSON`);
return (await response.json()) as T;
return { body: (await response.json()) as T, response };
}
async function expectText(url: string, status: number): Promise<string> {
async function expectText(url: string, status: number): Promise<{ body: string; response: Response }> {
const response = await fetch(url);
assert(response.status === status, `${url} 应返回 ${status},实际为 ${response.status}`);
return response.text();
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 assert(condition: boolean, message: string): asserts condition {