Files
dashlibstorage/generate-api-doc.js
seb aefd97a171 Rewrite store JSDoc, split quote/order into modules, and generate typed API docs
- Rewrite JSDoc across all store methods to document the actual async
  methods (real params, returns, throws) instead of the internal wrappers
- Split quote/order into individual module files backed by shared
  manifestDoc helpers; remove the createStore factory and thin wrappers
- Deduplicate manifest line helpers into src/store/manifestLines.js
- Extend generate-api-doc.js to render parameter/return/throws types and
  emit per-namespace API.<module>.md files with an API.md index
- Regenerate all API docs
2026-08-07 06:23:17 +02:00

297 lines
8.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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',
'resolveSelectedCompanyAccess.js',
'resolveCategoryAccess.js',
'resolveCustomerGroupAccess.js',
'resolveEntityAccess.js',
'resolvePicAccess.js',
'resolveArticleBySku.js',
'queryBySku.js',
'queryCategoryArticles.js',
'getCategoryPosterminalIds.js',
'nextSku.js',
'nextNumber.js',
'peekNumber.js',
'shape.js',
'createStore.js',
'manifestLines.js',
'index.js',
]);
/**
* Strip the outer braces from a JSDoc type (e.g. `{number}` -> `number`),
* preserving nested braces inside generics.
* @param {string} type
* @returns {string}
*/
function stripBraces(type) {
if (type.startsWith('{') && type.endsWith('}')) {
return type.slice(1, -1);
}
return type;
}
/**
* Extract the leading JSDoc block from a source string.
* @param {string} source
* @returns {{ description: string, example: string, params: Array<object>, returns: string|null, throws: string|null } | 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 = [];
const params = [];
let returns = null;
let throws = null;
let inExample = false;
for (const line of lines) {
const trimmed = line.trim();
if (trimmed === '@example') {
inExample = true;
continue;
}
if (trimmed.startsWith('@')) {
inExample = false;
const paramMatch = trimmed.match(/^@param\s+(\{.*\})\s+(\S+)\s*[-–]?\s*(.*)$/);
if (paramMatch) {
params.push({
type: stripBraces(paramMatch[1]),
name: paramMatch[2],
description: paramMatch[3].trim(),
});
continue;
}
const returnsMatch = trimmed.match(/^@returns\s+(\{.*\})\s*[-–]?\s*(.*)$/);
if (returnsMatch) {
returns = { type: stripBraces(returnsMatch[1]), description: returnsMatch[2].trim() };
continue;
}
const throwsMatch = trimmed.match(/^@throws\s+(\{.*\})\s*[-–]?\s*(.*)$/);
if (throwsMatch) {
throws = { type: stripBraces(throwsMatch[1]), description: throwsMatch[2].trim() };
continue;
}
continue;
}
if (inExample) {
example.push(line);
} else if (trimmed) {
description.push(trimmed);
}
}
return {
description: description.join(' ').trim(),
example: example.join('\n').trim(),
params,
returns,
throws,
};
}
/**
* 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,
params: doc.params,
returns: doc.returns,
throws: doc.throws,
});
}
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.params && entry.params.length) {
lines.push('**Parameters**');
lines.push('');
lines.push('| Name | Type | Description |');
lines.push('| --- | --- | --- |');
for (const p of entry.params) {
lines.push(`| \`${p.name}\` | \`${p.type}\` | ${p.description} |`);
}
lines.push('');
}
if (entry.returns) {
lines.push('**Returns**');
lines.push('');
lines.push(`\`${entry.returns.type}\`${entry.returns.description ? ` — ${entry.returns.description}` : ''}`);
lines.push('');
}
if (entry.throws) {
lines.push('**Throws**');
lines.push('');
lines.push(`\`${entry.throws.type}\`${entry.throws.description ? ` — ${entry.throws.description}` : ''}`);
lines.push('');
}
if (entry.example) {
lines.push('**Example**');
lines.push('');
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)`);
}