forked from Rosetta/landing
183 lines
5.6 KiB
JavaScript
183 lines
5.6 KiB
JavaScript
import { access, mkdir, readFile, readdir, writeFile } from 'node:fs/promises';
|
|
import { basename, resolve } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { isMap, isSeq, parseDocument, stringify } from 'yaml';
|
|
|
|
const projectRoot = resolve(fileURLToPath(new URL('..', import.meta.url)));
|
|
const sourceDirectory = resolve(
|
|
process.env.ROSETTA_PROTO_DIR || resolve(projectRoot, '../rcc/proto'),
|
|
);
|
|
const outputPath = resolve(projectRoot, 'src/generated/protoDocs.ts');
|
|
|
|
async function exists(path) {
|
|
try {
|
|
await access(path);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function cleanComment(...comments) {
|
|
return comments
|
|
.filter(Boolean)
|
|
.join('\n')
|
|
.split('\n')
|
|
.map((line) => line.replace(/^\s*#?\s?/, '').trim())
|
|
.filter(Boolean)
|
|
.join(' ');
|
|
}
|
|
|
|
function mapFields(node) {
|
|
if (!isMap(node)) {
|
|
return [];
|
|
}
|
|
|
|
const fields = node.get('fields', true);
|
|
if (!isSeq(fields)) {
|
|
return [];
|
|
}
|
|
|
|
return fields.items.map((field) => {
|
|
const json = field.toJSON();
|
|
const nameNode = field.get('name', true);
|
|
const typeNode = field.get('type', true);
|
|
const rawName = String(json.name);
|
|
const optional = rawName.endsWith('?');
|
|
|
|
return {
|
|
id: Number(json.id),
|
|
name: optional ? rawName.slice(0, -1) : rawName,
|
|
type: String(json.type),
|
|
optional,
|
|
description: cleanComment(
|
|
field.commentBefore,
|
|
field.comment,
|
|
nameNode?.commentBefore,
|
|
nameNode?.comment,
|
|
typeNode?.comment,
|
|
),
|
|
};
|
|
});
|
|
}
|
|
|
|
function mapNamedCollection(node, kind) {
|
|
if (!isMap(node)) {
|
|
return [];
|
|
}
|
|
|
|
return node.items.map((pair) => {
|
|
const name = String(pair.key.value);
|
|
const description = cleanComment(
|
|
pair.key.commentBefore,
|
|
pair.key.comment,
|
|
pair.value?.commentBefore,
|
|
pair.value?.comment,
|
|
);
|
|
const yaml = stringify(
|
|
{ [name]: pair.value.toJSON() },
|
|
{ indent: 2, lineWidth: 0 },
|
|
).trimEnd();
|
|
|
|
if (kind === 'enum') {
|
|
const values = pair.value.get('values', true);
|
|
return {
|
|
name,
|
|
type: String(pair.value.get('type')),
|
|
description,
|
|
yaml,
|
|
values: isSeq(values)
|
|
? values.items.map((value) => {
|
|
const json = value.toJSON();
|
|
const nameNode = value.get('name', true);
|
|
return {
|
|
id: Number(json.id),
|
|
name: String(json.name),
|
|
description: cleanComment(
|
|
value.commentBefore,
|
|
value.comment,
|
|
nameNode?.commentBefore,
|
|
nameNode?.comment,
|
|
),
|
|
};
|
|
})
|
|
: [],
|
|
};
|
|
}
|
|
|
|
const fields = mapFields(pair.value);
|
|
return {
|
|
name,
|
|
id: kind === 'packet' ? Number(pair.value.get('id')) : undefined,
|
|
description,
|
|
yaml,
|
|
fields,
|
|
};
|
|
});
|
|
}
|
|
|
|
async function readLayer(path) {
|
|
const source = await readFile(path, 'utf8');
|
|
const document = parseDocument(source, { keepSourceTokens: true });
|
|
|
|
if (document.errors.length > 0) {
|
|
throw new Error(
|
|
`${basename(path)}: ${document.errors.map((error) => error.message).join('; ')}`,
|
|
);
|
|
}
|
|
|
|
return {
|
|
version: Number(document.get('version')),
|
|
description: String(document.get('description') || ''),
|
|
source: basename(path),
|
|
enums: mapNamedCollection(document.get('enums', true), 'enum'),
|
|
types: mapNamedCollection(document.get('types', true), 'type'),
|
|
packets: mapNamedCollection(document.get('packets', true), 'packet'),
|
|
};
|
|
}
|
|
|
|
let sourceFiles = [];
|
|
try {
|
|
sourceFiles = (await readdir(sourceDirectory))
|
|
.filter((file) => /\.ya?ml$/i.test(file))
|
|
.sort((a, b) => a.localeCompare(b, undefined, { numeric: true }))
|
|
.map((file) => resolve(sourceDirectory, file));
|
|
} catch {
|
|
sourceFiles = [];
|
|
}
|
|
|
|
if (sourceFiles.length === 0) {
|
|
if (await exists(outputPath)) {
|
|
console.log(
|
|
`Proto sources are unavailable at ${sourceDirectory}; keeping the committed generated documentation.`,
|
|
);
|
|
process.exit(0);
|
|
}
|
|
|
|
throw new Error(`No YAML schema files found in ${sourceDirectory}`);
|
|
}
|
|
|
|
const layers = [];
|
|
for (const file of sourceFiles) {
|
|
layers.push(await readLayer(file));
|
|
}
|
|
layers.sort((a, b) => a.version - b.version);
|
|
|
|
const generatedSource = `/* This file is generated by scripts/generate-proto-docs.mjs. */\nimport type { ProtoLayer } from '../types/proto';\n\nexport const protoLayers = ${JSON.stringify(layers, null, 2)} satisfies ProtoLayer[];\n`;
|
|
|
|
await mkdir(resolve(projectRoot, 'src/generated'), { recursive: true });
|
|
await writeFile(outputPath, generatedSource);
|
|
|
|
const totals = layers.reduce(
|
|
(result, layer) => ({
|
|
enums: result.enums + layer.enums.length,
|
|
types: result.types + layer.types.length,
|
|
packets: result.packets + layer.packets.length,
|
|
}),
|
|
{ enums: 0, types: 0, packets: 0 },
|
|
);
|
|
|
|
console.log(
|
|
`Generated Proto docs from ${layers.length} layers: ${totals.packets} packets, ${totals.types} types, ${totals.enums} enums.`,
|
|
);
|