Thing/packages/eve-contracts/scripts/generate-contracts.mjs

79 lines
3.4 KiB
JavaScript

#!/usr/bin/env node
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { compile } from "json-schema-to-typescript";
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const repoRoot = resolve(packageRoot, "..", "..");
const generatedRoot = resolve(packageRoot, "src", "generated");
const contracts = [
["providerAdvertisement", "EveProviderAdvertisement", "provider-advertisement.ts", "gamecult.eve.provider_advertisement.v1.schema.json"],
["surface", "EveSurfaceDocument", "surface.ts", "gamecult.eve.surface.v1.schema.json"],
["commandDescriptor", "EveCommandDescriptor", "command-descriptor.ts", "gamecult.eve.command.v1.schema.json"],
["commandInvocation", "EveCommandInvocation", "command-invocation.ts", "gamecult.eve.command_invocation.v1.schema.json"],
["commandReceipt", "EveCommandReceipt", "command-receipt.ts", "gamecult.eve.command_receipt.v1.schema.json"],
["commandResult", "EveCommandResult", "command-result.ts", "gamecult.eve.command_result.v1.schema.json"],
["inputCapability", "EveInputCapability", "input-capability.ts", "gamecult.eve.input_capability.v1.schema.json"],
];
const loaded = [];
for (const [key, typeName, generatedFileName, schemaFileName] of contracts) {
const path = resolve(repoRoot, "schemas", schemaFileName);
const source = await readFile(path, "utf8");
const schema = JSON.parse(source);
loaded.push({ key, typeName, generatedFileName, schemaFileName, schema });
}
const schemasById = new Map(loaded.map(({ schema }) => [schema.$id, schema]));
for (const contract of loaded) {
const typeSchema = inlineKnownRefs(contract.schema);
contract.typeSource = (await compile(typeSchema, contract.typeName, {
bannerComment: "",
format: false,
unknownAny: false,
})).trim();
}
const schemasOutput = `${generatedHeader()}\nexport const eveContractSchemas = ${JSON.stringify(
Object.fromEntries(loaded.map(({ key, schema }) => [key, schema])),
null,
2,
)} as const;\n\nexport type EveContractSchemaName = keyof typeof eveContractSchemas;\n`;
await mkdir(generatedRoot, { recursive: true });
for (const { generatedFileName, schemaFileName, typeSource } of loaded) {
await checkOrWrite(
resolve(generatedRoot, generatedFileName),
`${generatedHeader()}\n// Source: schemas/${schemaFileName}\n${typeSource}\n`,
);
}
await checkOrWrite(resolve(generatedRoot, "schemas.ts"), schemasOutput);
function generatedHeader() {
return "// Generated by scripts/generate-contracts.mjs. Do not edit by hand.";
}
async function checkOrWrite(path, expected) {
if (process.argv.includes("--check")) {
const actual = await readFile(path, "utf8").catch(() => "");
if (normalizeNewlines(actual) !== normalizeNewlines(expected)) {
throw new Error(`${path} is stale. Run npm run generate in ${packageRoot}.`);
}
return;
}
await writeFile(path, expected, "utf8");
}
function normalizeNewlines(value) {
return value.replace(/\r\n/g, "\n");
}
function inlineKnownRefs(value) {
if (Array.isArray(value)) return value.map(inlineKnownRefs);
if (!value || typeof value !== "object") return value;
if (typeof value.$ref === "string" && schemasById.has(value.$ref)) {
return inlineKnownRefs(schemasById.get(value.$ref));
}
return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, inlineKnownRefs(child)]));
}