Files
dashlibstorage/generate-api-doc.js
2026-08-07 04:05:01 +02:00

226 lines
5.8 KiB
JavaScript

import { readdirSync, readFileSync, writeFileSync, statSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const storeDir = join(__dirname, 'src', 'store');
const outputPath = join(__dirname, 'API.md');
// Modules that are exposed on the LibDashStore instance (from src/index.js).
// Order here controls the order of sections in the generated document.
const MODULE_ORDER = [
'companyuser',
'company',
'category',
'article',
'lang',
'tax',
'unit',
'companysetting',
'brand',
'customergroup',
'entity',
'entityaddress',
'entitycontact',
'supplier',
'delivery',
'quote',
'order',
'posterminal',
'google_auth',
'pic',
];
// Files that are internal helpers and should not appear as public API entries.
const INTERNAL_FILES = new Set([
'request.js',
'slugify.js',
'initialize.js',
'purge.js',
'helpers.js',
'writeLines.js',
'shapeSetting.js',
'shapeUnit.js',
'ensureDefault.js',
'ensureLieferantenGroup.js',
'resolveCompanyAccess.js',
'resolveCategoryAccess.js',
'resolveCustomerGroupAccess.js',
'resolveEntityAccess.js',
'resolvePicAccess.js',
'resolveArticleBySku.js',
'queryBySku.js',
'queryCategoryArticles.js',
'getCategoryPosterminalIds.js',
'nextSku.js',
'createStore.js',
'index.js',
]);
/**
* Extract the leading JSDoc block from a source string.
* @param {string} source
* @returns {{ description: string, example: string } | null}
*/
function extractJsDoc(source) {
const match = source.match(/\/\*\*([\s\S]*?)\*\//);
if (!match) return null;
const block = match[1];
const lines = block
.split('\n')
.map((l) => l.replace(/^\s*\*\s?/, '').replace(/^\s*\*/, ''));
const description = [];
const example = [];
let inExample = false;
for (const line of lines) {
if (line.trim() === '@example') {
inExample = true;
continue;
}
if (line.trim().startsWith('@')) {
inExample = false;
continue;
}
if (inExample) {
example.push(line);
} else if (line.trim()) {
description.push(line.trim());
}
}
return {
description: description.join(' ').trim(),
example: example.join('\n').trim(),
};
}
/**
* Recursively collect all .js files under a directory.
* @param {string} dir
* @returns {string[]} absolute file paths
*/
function collectFiles(dir) {
const out = [];
for (const entry of readdirSync(dir)) {
const full = join(dir, entry);
if (statSync(full).isDirectory()) {
out.push(...collectFiles(full));
} else if (entry.endsWith('.js')) {
out.push(full);
}
}
return out;
}
/**
* Build a map of module name -> array of { name, file, description, example }.
* @returns {Map<string, Array<object>>}
*/
function buildModules() {
const modules = new Map();
for (const file of collectFiles(storeDir)) {
const rel = file.slice(storeDir.length + 1).split('/');
const moduleName = rel[0];
const fileName = rel[rel.length - 1];
if (INTERNAL_FILES.has(fileName)) continue;
const source = readFileSync(file, 'utf8');
const doc = extractJsDoc(source);
if (!doc) continue;
const fnName = fileName.replace(/\.js$/, '');
if (!modules.has(moduleName)) {
modules.set(moduleName, []);
}
modules.get(moduleName).push({
name: fnName,
file: rel.join('/'),
description: doc.description,
example: doc.example,
});
}
return modules;
}
function renderSection(moduleName, entries) {
const lines = [];
lines.push(`# ${moduleName}`);
lines.push('');
lines.push(
'This document is auto-generated from the JSDoc comments in `src/store` by running `npm run api-doc`.',
);
lines.push('');
for (const entry of entries) {
lines.push(`## \`${moduleName}.${entry.name}\``);
lines.push('');
lines.push(`*Source: \`src/store/${entry.file}\`*`);
lines.push('');
if (entry.description) {
lines.push(entry.description);
lines.push('');
}
if (entry.example) {
lines.push('```js');
lines.push(entry.example);
lines.push('```');
lines.push('');
}
}
return lines.join('\n');
}
function render() {
const modules = buildModules();
const ordered = MODULE_ORDER.filter((m) => modules.has(m));
const extra = [...modules.keys()].filter((m) => !MODULE_ORDER.includes(m)).sort();
const moduleNames = [...ordered, ...extra];
// Write one file per section: API.<section>.md
const written = [];
for (const moduleName of moduleNames) {
const sectionPath = join(__dirname, `API.${moduleName}.md`);
const markdown = renderSection(moduleName, modules.get(moduleName));
writeFileSync(sectionPath, markdown, 'utf8');
written.push({ moduleName, sectionPath, bytes: markdown.length });
}
// Write the index API.md linking to each section file.
const lines = [];
lines.push('# LibDashStore API');
lines.push('');
lines.push(
'This document is auto-generated from the JSDoc comments in `src/store` by running `npm run api-doc`.',
);
lines.push('');
lines.push(
'The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each method is an async function that takes a `pool` (mssql connection pool) at construction time and returns a callable function. Methods are grouped by the namespace they are exposed under.',
);
lines.push('');
lines.push('## Table of Contents');
lines.push('');
for (const { moduleName } of written) {
lines.push(`- [${moduleName}](API.${moduleName}.md)`);
}
lines.push('');
return { index: lines.join('\n'), written };
}
const { index, written } = render();
writeFileSync(outputPath, index, 'utf8');
console.log(`Wrote ${outputPath} (${index.length} bytes)`);
for (const { moduleName, sectionPath, bytes } of written) {
console.log(`Wrote ${sectionPath} (${bytes} bytes)`);
}