forked from Rosetta/landing
Redesign landing and generate Proto docs
This commit is contained in:
176
scripts/generate-proto-docs.mjs
Normal file
176
scripts/generate-proto-docs.mjs
Normal file
@@ -0,0 +1,176 @@
|
||||
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 } 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,
|
||||
);
|
||||
|
||||
if (kind === 'enum') {
|
||||
const values = pair.value.get('values', true);
|
||||
return {
|
||||
name,
|
||||
type: String(pair.value.get('type')),
|
||||
description,
|
||||
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,
|
||||
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} objects, ${totals.enums} enums.`,
|
||||
);
|
||||
Reference in New Issue
Block a user