164 lines
8.4 KiB
JavaScript
164 lines
8.4 KiB
JavaScript
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();
|