Files
quixos-protocol/src/descriptor.ts
T

76 lines
2.5 KiB
TypeScript

import childProcess from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import { fromBinary, toJsonString } from "@bufbuild/protobuf";
import type { PackageDescriptor } from "./gen/quixos/package_pb.js";
import { PackageDescriptorSchema } from "./gen/quixos/package_pb.js";
const protoPaths = () => {
const candidates = [
...(process.env.QUIXOS_PROTO_PATH ?? "")
.split(path.delimiter)
.map((entry) => entry.trim())
.filter(Boolean),
path.resolve(process.cwd(), "proto"),
path.resolve(process.cwd(), "quixos-protocol/proto"),
];
return [...new Set(candidates)].filter((candidate) => fs.existsSync(path.join(candidate, "quixos/package.proto")));
};
export const parsePackageDescriptorTextproto = (text: string): PackageDescriptor => {
const paths = protoPaths();
if (paths.length === 0) {
throw new Error("QUIXOS_PROTO_PATH must include quixos-protocol/proto to parse package descriptors");
}
const result = childProcess.spawnSync(
"protoc",
[
...paths.map((protoPath) => `--proto_path=${protoPath}`),
"--encode=quixos.PackageDescriptor",
"quixos/package.proto",
],
{
input: Buffer.from(text),
maxBuffer: 1024 * 1024 * 8,
},
);
if (result.error) {
throw result.error;
}
if (result.status !== 0) {
throw new Error(`Failed to parse package descriptor textproto:\n${result.stderr.toString().trim()}`);
}
return fromBinary(PackageDescriptorSchema, result.stdout);
};
export const packageDescriptorToJson = (descriptor: PackageDescriptor) =>
toJsonString(PackageDescriptorSchema, descriptor, {
prettySpaces: 2,
});
export const validatePackageDescriptor = (descriptor: PackageDescriptor): string[] => {
const errors: string[] = [];
if (!descriptor.packageId) {
errors.push("packageId is required");
}
if (!descriptor.packageRevisionId) {
errors.push("packageRevisionId is required");
}
if (!descriptor.runtimeProtocolVersion) {
errors.push("runtimeProtocolVersion is required");
}
const seenExports = new Set<string>();
for (const [index, entry] of descriptor.exports.entries()) {
if (!entry.exportId) errors.push(`exports[${index}].exportId is required`);
if (!entry.runtimeSymbol) {
errors.push(`exports[${index}].runtimeSymbol is required`);
}
if (seenExports.has(entry.exportId)) {
errors.push(`duplicate export: ${entry.exportId}`);
}
seenExports.add(entry.exportId);
}
return errors;
};