81 lines
2.9 KiB
JavaScript
81 lines
2.9 KiB
JavaScript
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import process from "node:process";
|
|
|
|
const repoRoot = process.cwd();
|
|
const mediaExtensions = new Set([".png", ".jpg", ".jpeg", ".webp", ".gif", ".mp4", ".mov", ".webm", ".avif"]);
|
|
const textExtensions = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".json", ".html", ".css", ".md", ".env", ".example"]);
|
|
|
|
const scanRoots = ["src", "vite.config.ts", "index.html", "package.json", ".env.example"];
|
|
const allowedFiles = new Set([
|
|
normalizePath("src/data/ossAssets.ts"),
|
|
normalizePath("src/utils/ossImageOptimize.ts"),
|
|
]);
|
|
|
|
const forbiddenPatterns = [
|
|
{ label: "frontend env config", pattern: /\b(?:import\.meta\.env|VITE_[A-Z0-9_]+)\b/ },
|
|
{ label: "direct provider proxy", pattern: /\/dashscope-api\b|dashscope\.aliyuncs\.com/i },
|
|
{ label: "third-party demo media host", pattern: /picsum\.photos|xiuxiu-pro(?:-new)?\.meitudata\.com|meitudata\.com/i },
|
|
{ label: "hard-coded provider secret marker", pattern: /Bearer\s+sk-|DASHSCOPE_API_KEY|ACCESS_KEY_SECRET|SECRET_ACCESS_KEY/i },
|
|
{ label: "local media import", pattern: /from\s+["'][^"']*\/assets\/[^"']*\.(?:png|jpe?g|webp|gif|mp4|mov|webm|avif|svg)["']/i },
|
|
];
|
|
|
|
const failures = [];
|
|
|
|
function normalizePath(value) {
|
|
return value.replace(/\\/g, "/");
|
|
}
|
|
|
|
function walk(targetPath, visitor) {
|
|
if (!fs.existsSync(targetPath)) return;
|
|
const stat = fs.statSync(targetPath);
|
|
if (stat.isDirectory()) {
|
|
for (const entry of fs.readdirSync(targetPath)) {
|
|
if (entry === "node_modules" || entry === "dist" || entry === ".git") continue;
|
|
walk(path.join(targetPath, entry), visitor);
|
|
}
|
|
return;
|
|
}
|
|
visitor(targetPath, stat);
|
|
}
|
|
|
|
function report(file, message) {
|
|
failures.push(`${normalizePath(path.relative(repoRoot, file))}: ${message}`);
|
|
}
|
|
|
|
walk(path.join(repoRoot, "src", "assets"), (file) => {
|
|
if (mediaExtensions.has(path.extname(file).toLowerCase())) {
|
|
report(file, "media files must live in OSS, not src/assets");
|
|
}
|
|
});
|
|
|
|
for (const root of scanRoots) {
|
|
walk(path.join(repoRoot, root), (file) => {
|
|
const relative = normalizePath(path.relative(repoRoot, file));
|
|
const ext = path.extname(file).toLowerCase();
|
|
if (!textExtensions.has(ext) && !relative.endsWith(".env.example")) return;
|
|
if (relative.startsWith("src/assets/")) return;
|
|
|
|
const content = fs.readFileSync(file, "utf8");
|
|
const isAllowed = allowedFiles.has(relative);
|
|
for (const rule of forbiddenPatterns) {
|
|
if (isAllowed && (rule.label === "third-party demo media host" || rule.label === "hard-coded provider secret marker")) {
|
|
continue;
|
|
}
|
|
if (rule.pattern.test(content)) {
|
|
report(file, `forbidden ${rule.label}`);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
if (failures.length) {
|
|
console.error("Governance check failed:");
|
|
for (const failure of failures) {
|
|
console.error(`- ${failure}`);
|
|
}
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log("Governance check passed.");
|