feat(scanner): complete localized artifact quality checkpoint

This commit is contained in:
AzuTear
2026-07-11 15:59:19 +02:00
parent 639b0b7f59
commit 8b9f948c6b
215 changed files with 35440 additions and 7273 deletions
+163
View File
@@ -0,0 +1,163 @@
import fs from "node:fs";
import path from "node:path";
import { pathToFileURL } from "node:url";
import { extractFile, listPackage } from "@electron/asar";
export function rendererAssetReferences(html) {
return [...String(html).matchAll(/\b(?:src|href)\s*=\s*["']([^"']+)["']/gi)]
.map((match) => match[1].trim())
.filter(Boolean);
}
export function absoluteRendererAssetReferences(html) {
return rendererAssetReferences(html).filter((reference) => reference.startsWith("/"));
}
export function runtimeSignature(source) {
return String(source).match(/APP_RUNTIME_SIGNATURE\s*=\s*["']([^"']+)["']/)?.[1] ?? "";
}
export function verifyProjectPackaging(projectRoot = process.cwd()) {
const packageJson = JSON.parse(fs.readFileSync(path.join(projectRoot, "package.json"), "utf8"));
const checks = [];
const viteConfigPath = path.join(projectRoot, "vite.config.ts");
const viteConfig = readText(viteConfigPath);
const rendererIndexPath = path.join(projectRoot, "dist", "index.html");
const rendererIndex = readText(rendererIndexPath);
const mainSource = readText(path.join(projectRoot, "electron", "main.ts"));
const sourceRuntimeSignature = runtimeSignature(mainSource);
check(checks, packageJson.main === "dist-electron/electron/main.js", "main-entry", packageJson.main);
check(checks, packageJson.build?.win?.requestedExecutionLevel === "requireAdministrator", "windows-elevation", packageJson.build?.win?.requestedExecutionLevel);
check(checks, /\bbase\s*:\s*["']\.\/["']/.test(viteConfig), "vite-relative-base", "Vite base must be ./ for Electron loadFile");
check(checks, resourceMapping(packageJson, "native/input-helper/bin/publish", "input-helper"), "helper-resource-mapping", "native helper -> resources/input-helper");
check(checks, resourceMapping(packageJson, "data/ik-inventorylists", "ik-inventorylists"), "ik-resource-mapping", "IK lists -> resources/ik-inventorylists");
checkFile(checks, path.join(projectRoot, "native", "input-helper", "bin", "publish", "InputHelper.exe"), "compiled-helper", 1_000_000);
checkFile(checks, path.join(projectRoot, "data", "ik-inventorylists", "artifacts.json"), "ik-artifacts", 1_000);
checkFile(checks, path.join(projectRoot, "data", "ik-inventorylists", "version.txt"), "ik-version", 1);
checkFile(checks, rendererIndexPath, "compiled-renderer-index", 100);
checkFile(checks, path.join(projectRoot, "dist-electron", "electron", "preload.cjs"), "compiled-preload", 100);
checkFile(checks, path.join(projectRoot, "dist-electron", "electron", "runtimePaths.js"), "compiled-runtime-path-policy", 100);
if (rendererIndex) checkRendererAssetPolicy(checks, rendererIndex, "project-renderer");
check(checks, sourceRuntimeSignature.startsWith("2026-07-10-"), "current-runtime-signature", sourceRuntimeSignature || "missing");
return packagingReport("project", projectRoot, checks);
}
export function verifyPackagedDirectory(packageDir, projectRoot = process.cwd()) {
const resolved = path.resolve(packageDir);
const resources = path.join(resolved, "resources");
const asarPath = path.join(resources, "app.asar");
const checks = [];
checkFile(checks, path.join(resolved, "Genshin Artifact Assistant.exe"), "packaged-executable", 1_000_000);
checkFile(checks, asarPath, "app-asar", 1_000);
checkFile(checks, path.join(resources, "input-helper", "InputHelper.exe"), "packaged-helper", 1_000_000);
checkFile(checks, path.join(resources, "ik-inventorylists", "artifacts.json"), "packaged-ik-artifacts", 1_000);
checkFile(checks, path.join(resources, "ik-inventorylists", "version.txt"), "packaged-ik-version", 1);
if (fs.existsSync(asarPath)) {
const files = new Set(listPackage(asarPath).map((entry) => entry.replaceAll("\\", "/").replace(/^\//, "")));
for (const expected of ["dist/index.html", "dist-electron/electron/main.js", "dist-electron/electron/preload.cjs", "dist-electron/electron/runtimePaths.js", "package.json"]) {
check(checks, files.has(expected), `asar:${expected}`, expected);
}
if (files.has("dist/index.html")) {
const rendererIndex = extractFile(asarPath, asarExtractPath("dist/index.html")).toString("utf8");
checkRendererAssetPolicy(checks, rendererIndex, "packaged-renderer");
const missingAssets = rendererAssetReferences(rendererIndex)
.filter(isLocalRendererReference)
.map((reference) => rendererReferenceAsarPath(reference))
.filter((reference) => !files.has(reference));
check(checks, missingAssets.length === 0, "packaged-renderer-assets-present", missingAssets.join(", ") || "all referenced assets present");
}
if (files.has("dist-electron/electron/main.js")) {
const packagedMain = extractFile(asarPath, asarExtractPath("dist-electron/electron/main.js")).toString("utf8");
const sourceMain = readText(path.join(projectRoot, "electron", "main.ts"));
const sourceSignature = runtimeSignature(sourceMain);
const packagedSignature = runtimeSignature(packagedMain);
check(
checks,
Boolean(sourceSignature) && packagedSignature === sourceSignature,
"runtime-signature-match",
`${sourceSignature || "missing source"} == ${packagedSignature || "missing package"}`,
);
}
}
const sourceVersion = fs.readFileSync(path.join(projectRoot, "data", "ik-inventorylists", "version.txt"), "utf8").trim();
const packagedVersionPath = path.join(resources, "ik-inventorylists", "version.txt");
const packagedVersion = fs.existsSync(packagedVersionPath) ? fs.readFileSync(packagedVersionPath, "utf8").trim() : "";
check(checks, sourceVersion === packagedVersion, "ik-version-match", `${sourceVersion} == ${packagedVersion || "missing"}`);
return packagingReport("packaged", resolved, checks);
}
function resourceMapping(packageJson, from, to) {
return (packageJson.build?.extraResources ?? []).some((entry) => entry.from === from && entry.to === to);
}
function readText(filePath) {
return fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf8") : "";
}
function checkRendererAssetPolicy(checks, html, idPrefix) {
const absoluteReferences = absoluteRendererAssetReferences(html);
check(
checks,
absoluteReferences.length === 0,
`${idPrefix}-assets-relative`,
absoluteReferences.join(", ") || "all src/href references are relative",
);
}
function isLocalRendererReference(reference) {
return !reference.startsWith("#") && !/^[a-z][a-z\d+.-]*:/i.test(reference);
}
function rendererReferenceAsarPath(reference) {
const withoutQuery = reference.split(/[?#]/, 1)[0].replace(/^\.\//, "");
return path.posix.normalize(path.posix.join("dist", withoutQuery));
}
function asarExtractPath(reference) {
return reference.replaceAll("/", path.sep);
}
function checkFile(checks, filePath, id, minimumBytes) {
const exists = fs.existsSync(filePath);
const size = exists ? fs.statSync(filePath).size : 0;
check(checks, exists && size >= minimumBytes, id, `${filePath} (${size} bytes)`);
}
function check(checks, ok, id, detail) {
checks.push({ id, ok: Boolean(ok), detail: String(detail ?? "") });
}
function packagingReport(mode, input, checks) {
return {
version: "packaging-offline-v2",
mode,
input,
ok: checks.every((entry) => entry.ok),
passed: checks.filter((entry) => entry.ok).length,
failed: checks.filter((entry) => !entry.ok).length,
checks,
};
}
async function main() {
const staticOnly = process.argv.includes("--static");
const inputArg = process.argv.find((argument) => argument.startsWith("--input="));
const reports = [verifyProjectPackaging()];
if (!staticOnly) {
const input = inputArg?.slice("--input=".length) || path.resolve("outputs", "dist", "win-unpacked");
reports.push(verifyPackagedDirectory(input));
}
const result = { version: "packaging-offline-suite-v2", ok: reports.every((report) => report.ok), reports };
const outputDir = path.resolve("outputs", "packaging");
fs.mkdirSync(outputDir, { recursive: true });
const reportPath = path.join(outputDir, "offline-package-report.json");
fs.writeFileSync(reportPath, JSON.stringify(result, null, 2), "utf8");
for (const report of reports) {
console.log(`${report.mode}: ${report.passed}/${report.checks.length} checks passed`);
for (const failed of report.checks.filter((entry) => !entry.ok)) console.error(`FAIL ${failed.id}: ${failed.detail}`);
}
console.log(`Report: ${reportPath}`);
if (!result.ok) process.exitCode = 1;
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) await main();