chore: simplify frontend verification
This commit is contained in:
@@ -1,100 +0,0 @@
|
||||
import { statSync } from "node:fs";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { resolve } from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
const INITIAL_BUNDLE_BUDGET_BYTES = 500 * 1024;
|
||||
|
||||
function manifestEntries(manifest) {
|
||||
if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) {
|
||||
throw new Error("Vite manifest must be an object");
|
||||
}
|
||||
return Object.entries(manifest);
|
||||
}
|
||||
|
||||
function assetFile(entry, entryName) {
|
||||
if (!entry || typeof entry.file !== "string" || !entry.file.endsWith(".js")) {
|
||||
throw new Error(
|
||||
`Manifest entry '${entryName}' must reference a JavaScript file`,
|
||||
);
|
||||
}
|
||||
return entry.file;
|
||||
}
|
||||
|
||||
function getInitialAssetFiles(manifest) {
|
||||
const entries = manifestEntries(manifest);
|
||||
const entryCandidates = entries.filter(
|
||||
([, entry]) => entry?.isEntry === true,
|
||||
);
|
||||
if (entryCandidates.length !== 1) {
|
||||
throw new Error("Vite manifest must contain exactly one JavaScript entry");
|
||||
}
|
||||
|
||||
const files = [];
|
||||
const visitedEntries = new Set();
|
||||
const visit = (entryName) => {
|
||||
if (visitedEntries.has(entryName)) return;
|
||||
visitedEntries.add(entryName);
|
||||
const entry = manifest[entryName];
|
||||
if (!entry)
|
||||
throw new Error(`Vite manifest is missing eager import '${entryName}'`);
|
||||
files.push(assetFile(entry, entryName));
|
||||
for (const importedEntry of entry.imports ?? []) visit(importedEntry);
|
||||
};
|
||||
|
||||
visit(entryCandidates[0][0]);
|
||||
return files;
|
||||
}
|
||||
|
||||
function measureAssets(files, getSize) {
|
||||
return files.reduce((total, file) => {
|
||||
const size = getSize(file);
|
||||
if (!Number.isSafeInteger(size) || size < 0) {
|
||||
throw new Error(`Cannot measure bundle asset '${file}'`);
|
||||
}
|
||||
return total + size;
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function assertBundleBudget(initialBytes) {
|
||||
if (initialBytes > INITIAL_BUNDLE_BUDGET_BYTES) {
|
||||
throw new Error(
|
||||
`Initial JavaScript payload is ${initialBytes} bytes; budget is ${INITIAL_BUNDLE_BUDGET_BYTES} bytes`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const distributionDirectory = resolve("dist");
|
||||
const manifestPath = resolve(distributionDirectory, "bundle-manifest.json");
|
||||
|
||||
async function loadJson(path) {
|
||||
return JSON.parse(await readFile(path, "utf8"));
|
||||
}
|
||||
|
||||
function getAssetSize(file) {
|
||||
return statSync(resolve(distributionDirectory, file)).size;
|
||||
}
|
||||
|
||||
async function run() {
|
||||
const manifest = await loadJson(manifestPath);
|
||||
const initialBytes = measureAssets(
|
||||
getInitialAssetFiles(manifest),
|
||||
getAssetSize,
|
||||
);
|
||||
assertBundleBudget(initialBytes);
|
||||
console.log(`Initial JavaScript payload: ${initialBytes} bytes`);
|
||||
}
|
||||
|
||||
if (
|
||||
process.argv[1] &&
|
||||
import.meta.url === pathToFileURL(process.argv[1]).href
|
||||
) {
|
||||
await run();
|
||||
}
|
||||
|
||||
export {
|
||||
assertBundleBudget,
|
||||
getInitialAssetFiles,
|
||||
INITIAL_BUNDLE_BUDGET_BYTES,
|
||||
measureAssets,
|
||||
};
|
||||
@@ -1,36 +0,0 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
const snapshot = "src/shared/api/spec/openapi.json";
|
||||
const generated = "src/shared/api/generated/schema.d.ts";
|
||||
const directory = await mkdtemp(join(tmpdir(), "wealth-ledger-openapi-"));
|
||||
const expected = join(directory, "schema.d.ts");
|
||||
|
||||
try {
|
||||
await new Promise((resolve, reject) => {
|
||||
const child = spawn(
|
||||
"npx",
|
||||
["--no-install", "openapi-typescript", snapshot, "-o", expected],
|
||||
{
|
||||
stdio: "inherit",
|
||||
},
|
||||
);
|
||||
child.on("error", reject);
|
||||
child.on("exit", (code) =>
|
||||
code === 0
|
||||
? resolve()
|
||||
: reject(new Error(`openapi-typescript exited with ${code}`)),
|
||||
);
|
||||
});
|
||||
if (
|
||||
(await readFile(expected, "utf8")) !== (await readFile(generated, "utf8"))
|
||||
) {
|
||||
throw new Error(
|
||||
"Generated OpenAPI types drift from src/shared/api/spec/openapi.json. Run npm run openapi:generate.",
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
await rm(directory, { recursive: true, force: true });
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import { mkdtemp, rename, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
const baseUrl = process.env.VITE_API_BASE_URL ?? "http://localhost:3000";
|
||||
const snapshot = "src/shared/api/spec/openapi.json";
|
||||
const generated = "src/shared/api/generated/schema.d.ts";
|
||||
const directory = await mkdtemp(join(tmpdir(), "wealth-ledger-openapi-"));
|
||||
const nextSnapshot = join(directory, "openapi.json");
|
||||
const nextGenerated = join(directory, "schema.d.ts");
|
||||
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}/api/docs-json`, {
|
||||
headers: { Accept: "application/json" },
|
||||
});
|
||||
if (!response.ok)
|
||||
throw new Error(
|
||||
`OpenAPI sync failed: ${response.status} ${response.statusText}`,
|
||||
);
|
||||
const document = await response.json();
|
||||
if (!document || typeof document !== "object")
|
||||
throw new Error("OpenAPI sync failed: endpoint did not return an object");
|
||||
await writeFile(nextSnapshot, `${JSON.stringify(document, null, 2)}\n`);
|
||||
await new Promise((resolve, reject) => {
|
||||
const child = spawn(
|
||||
"npx",
|
||||
["--no-install", "openapi-typescript", nextSnapshot, "-o", nextGenerated],
|
||||
{
|
||||
stdio: "inherit",
|
||||
},
|
||||
);
|
||||
child.on("error", reject);
|
||||
child.on("exit", (code) =>
|
||||
code === 0
|
||||
? resolve()
|
||||
: reject(new Error(`openapi-typescript exited with ${code}`)),
|
||||
);
|
||||
});
|
||||
await rename(nextSnapshot, snapshot);
|
||||
await rename(nextGenerated, generated);
|
||||
} finally {
|
||||
await rm(directory, { recursive: true, force: true });
|
||||
}
|
||||
Reference in New Issue
Block a user