This commit is contained in:
seb
2026-09-30 18:45:30 +02:00
parent caacbe4968
commit d6fe6ff1d2
5 changed files with 298 additions and 15609 deletions

View File

@@ -393,7 +393,7 @@ function renderReadme(operations) {
lines.push(`| [\`routes/${op.file}\`](routes/${op.file}) | \`${op.method.toUpperCase()} ${op.path}\` |`);
}
lines.push('');
lines.push('Machine-readable spec: [`openapi.json`](openapi.json).');
lines.push('Route overview: [`routes.md`](routes.md).');
lines.push('');
lines.push('## Signals (SSE)');
lines.push('');
@@ -436,13 +436,127 @@ function renderReadme(operations) {
return lines.join('\n');
}
function summarizeSchemaCompact(schema) {
if (!schema) return '';
if (schema.$ref) return schemaRefName(schema);
if (schema.type === 'array') {
const item = summarizeSchemaCompact(schema.items) || 'any';
return `${item}[]`;
}
if (schema.type === 'object' || schema.properties) {
const props = Object.keys(schema.properties || {});
if (props.length === 0) return 'object';
if (props.length === 1) {
const k = props[0];
const v = summarizeSchemaCompact(schema.properties[k]);
return v ? `{ ${k}: ${v} }` : `{ ${k} }`;
}
if (props.length <= 3) return `{ ${props.join(', ')} }`;
return `{ ${props.slice(0, 3).join(', ')}, … }`;
}
if (schema.type) return schema.type;
return '';
}
function summarizeOperationParams(operation) {
const parts = [];
const params = operation.parameters || [];
for (const p of params) {
if (p.in === 'header') continue;
const prefix = p.in === 'query' ? '?' : '';
const type = p.schema?.type ? `:${p.schema.type}` : '';
parts.push(`${prefix}${p.name}${type}`);
}
if (operation.requestBody?.content) {
const content = operation.requestBody.content;
if (content['application/json']?.schema) {
const s = content['application/json'].schema;
const ref = schemaRefName(s);
if (ref) {
parts.push(`body: ${ref}`);
} else {
const sub = summarizeSchemaCompact(s);
parts.push(sub ? `body: ${sub}` : 'body: json');
}
} else if (content['multipart/form-data']) {
const props = Object.keys(content['multipart/form-data'].schema?.properties || {});
parts.push(props.length ? `form: { ${props.join(', ')} }` : 'form-data');
} else {
const ct = Object.keys(content)[0];
parts.push(ct);
}
}
return parts.length ? parts.join(', ') : '—';
}
function summarizeOperationReturns(operation) {
const responses = operation.responses || {};
const successCodes = Object.keys(responses)
.filter((c) => c.startsWith('2'))
.sort();
if (!successCodes.length) return '—';
const parts = [];
for (const code of successCodes) {
const resp = responses[code];
if (code === '204') {
parts.push('204 void');
continue;
}
if (!resp?.content) {
parts.push(code);
continue;
}
const ct = Object.keys(resp.content)[0];
if (ct === 'text/event-stream') {
parts.push(`${code} SSE`);
continue;
}
if (ct.startsWith('image/')) {
parts.push(`${code} ${ct.split('/')[1]}`);
continue;
}
if (ct === 'application/zip') {
parts.push(`${code} zip`);
continue;
}
const s = resp.content[ct]?.schema;
const sub = summarizeSchemaCompact(s);
if (sub) {
parts.push(`${code} ${sub}`);
} else {
parts.push(code);
}
}
return parts.join(', ');
}
function renderRoutesOverview(operations) {
const lines = [];
lines.push('# Dash API Routes Overview');
lines.push('');
lines.push(`Overview of all ${operations.length} API endpoints across all modules.`);
lines.push('');
lines.push('| Method | Route | Params | Return Type | Summary | Docs |');
lines.push('|:---|:---|:---|:---|:---|:---|');
for (const op of operations) {
const method = op.method.toUpperCase();
const summary = (op.summary || '').replace(/[\r\n|]+/g, ' ').trim();
const paramsStr = op.params === '—' ? '—' : `\`${op.params.replace(/\|/g, '\\|')}\``;
const returnsStr = op.returns === '—' ? '—' : `\`${op.returns.replace(/\|/g, '\\|')}\``;
lines.push(`| \`${method}\` | \`${op.path}\` | ${paramsStr} | ${returnsStr} | ${summary} | [\`routes/${op.file}\`](routes/${op.file}) |`);
}
lines.push('');
return lines.join('\n');
}
// --- generate ---
ensureDir(docsDir);
rmDirContents(routesDocsDir);
ensureDir(routesDocsDir);
fs.writeFileSync(path.join(docsDir, 'openapi.json'), JSON.stringify(spec, null, 2) + '\n');
const operations = [];
const paths = Object.keys(spec.paths || {}).sort();
@@ -455,10 +569,19 @@ for (const apiPath of paths) {
const absFile = path.join(routesDocsDir, relFile);
ensureDir(path.dirname(absFile));
fs.writeFileSync(absFile, renderOperation(apiPath, method, operation));
operations.push({ path: apiPath, method, file: relFile.replace(/\\/g, '/') });
operations.push({
path: apiPath,
method,
file: relFile.replace(/\\/g, '/'),
summary: operation.summary || '',
tags: operation.tags || [],
params: summarizeOperationParams(operation),
returns: summarizeOperationReturns(operation),
});
}
}
fs.writeFileSync(path.join(docsDir, 'routes.md'), renderRoutesOverview(operations));
fs.writeFileSync(path.join(docsDir, 'README.md'), renderReadme(operations));
console.log(`Wrote ${operations.length} route docs + openapi.json → docs/api/`);
console.log(`Wrote ${operations.length} route docs + routes.md → docs/api/`);