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

@@ -15,19 +15,31 @@ export function createFetchHandler(options: AppOptions) {
const url = new URL(request.url);
if (url.pathname === "/health") {
return Response.json(createHealthResponse());
if (!allowsGetHead(request.method)) {
return methodNotAllowedResponse(["GET", "HEAD"], options.mode);
}
return jsonResponse(createHealthResponse(), { method: request.method, mode: options.mode });
}
if (url.pathname === "/api/demo") {
return Response.json(createDemoResponse(options.mode));
if (!allowsGetHead(request.method)) {
return methodNotAllowedResponse(["GET", "HEAD"], options.mode);
}
return jsonResponse(createDemoResponse(options.mode), { method: request.method, mode: options.mode });
}
if (url.pathname.startsWith("/api/")) {
return Response.json(createApiError("API route not found", 404), { status: 404 });
return jsonResponse(createApiError("API route not found", 404), {
method: request.method,
mode: options.mode,
status: 404,
});
}
if (options.staticAssets) {
return serveStaticAsset(url.pathname, options.staticAssets);
return serveStaticAsset(url.pathname, options.staticAssets, options.mode);
}
return new Response("开发期请通过 Vite 前端地址访问页面。", {
@@ -62,38 +74,80 @@ function createApiError(error: string, status: number): ApiErrorResponse {
return { error, status };
}
function serveStaticAsset(pathname: string, staticAssets: StaticAssets): Response {
function allowsGetHead(method: string): boolean {
return method === "GET" || method === "HEAD";
}
function methodNotAllowedResponse(allow: string[], mode: RuntimeMode): Response {
return jsonResponse(createApiError("Method not allowed", 405), {
mode,
status: 405,
headers: { Allow: allow.join(", ") },
});
}
function jsonResponse(
body: ApiErrorResponse | DemoResponse | HealthResponse,
options: { method?: string; mode: RuntimeMode; status?: number; headers?: HeadersInit },
): Response {
const headers = createHeaders(options.mode, {
"Content-Type": "application/json; charset=utf-8",
...options.headers,
});
const responseBody = options.method === "HEAD" ? null : JSON.stringify(body);
return new Response(responseBody, {
status: options.status,
headers,
});
}
function serveStaticAsset(pathname: string, staticAssets: StaticAssets, mode: RuntimeMode): Response {
if (pathname === "/") {
return htmlResponse(staticAssets.indexHtml);
return htmlResponse(staticAssets.indexHtml, mode);
}
const asset = staticAssets.files[pathname];
if (asset) {
return new Response(asset, {
headers: {
headers: createHeaders(mode, {
"Content-Type": contentTypeFor(pathname),
"Cache-Control": "public, max-age=31536000, immutable",
},
}),
});
}
if (pathname.startsWith("/assets/") || hasFileExtension(pathname)) {
return new Response("Not Found", { status: 404 });
return new Response("Not Found", {
status: 404,
headers: createHeaders(mode, { "Content-Type": "text/plain; charset=utf-8" }),
});
}
return htmlResponse(staticAssets.indexHtml);
return htmlResponse(staticAssets.indexHtml, mode);
}
function htmlResponse(indexHtml: Blob): Response {
function htmlResponse(indexHtml: Blob, mode: RuntimeMode): Response {
return new Response(indexHtml, {
headers: {
headers: createHeaders(mode, {
"Content-Type": "text/html; charset=utf-8",
"Cache-Control": "no-cache",
},
}),
});
}
function createHeaders(mode: RuntimeMode, init: HeadersInit): Headers {
const headers = new Headers(init);
if (mode === "production") {
headers.set("X-Content-Type-Options", "nosniff");
headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
}
return headers;
}
function hasFileExtension(pathname: string): boolean {
return /\/[^/]+\.[^/]+$/.test(pathname);
}