346 lines
9.8 KiB
JavaScript
346 lines
9.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',
|
||
'articlesupplier',
|
||
'delivery',
|
||
'quote',
|
||
'order',
|
||
'manifestattachment',
|
||
'posterminal',
|
||
'google_auth',
|
||
'pic',
|
||
'numbers',
|
||
];
|
||
|
||
// 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',
|
||
'resolveManifestAccess.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,
|
||
});
|
||
}
|
||
|
||
for (const entries of modules.values()) {
|
||
entries.sort((a, b) => a.name.localeCompare(b.name));
|
||
}
|
||
|
||
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 formatParamType(param, allParams) {
|
||
const cleanName = param.name.replace(/^\[|\]$/g, '').split('=')[0].trim();
|
||
const prefix = cleanName + '.';
|
||
const childParams = allParams.filter((p) => {
|
||
const pClean = p.name.replace(/^\[|\]$/g, '').split('=')[0].trim();
|
||
return pClean.startsWith(prefix) && !pClean.slice(prefix.length).includes('.');
|
||
});
|
||
|
||
if (childParams.length > 0) {
|
||
const props = childParams.map((cp) => {
|
||
const isOpt = cp.name.startsWith('[');
|
||
const propName = cp.name.replace(/^\[|\]$/g, '').split('=')[0].trim().slice(prefix.length);
|
||
const propType = formatParamType(cp, allParams);
|
||
return `${propName}${isOpt ? '?' : ''}: ${propType}`;
|
||
});
|
||
return `{${props.join(', ')}}`;
|
||
}
|
||
|
||
return param.type || 'any';
|
||
}
|
||
|
||
function formatMethodSignature(entry) {
|
||
const topLevelParams = (entry.params || []).filter((p) => !p.name.includes('.'));
|
||
const paramsStr = topLevelParams
|
||
.map((p) => {
|
||
const isOptional = p.name.startsWith('[');
|
||
const cleanName = p.name.replace(/^\[|\]$/g, '').split('=')[0].trim();
|
||
const typeStr = formatParamType(p, entry.params || []);
|
||
return `${cleanName}${isOptional ? '?' : ''}: ${typeStr}`;
|
||
})
|
||
.join(', ');
|
||
|
||
const returnType = entry.returns?.type ? `: ${entry.returns.type}` : ': void';
|
||
return `(${paramsStr})${returnType}`;
|
||
}
|
||
|
||
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)`);
|
||
const entries = modules.get(moduleName) || [];
|
||
for (const entry of entries) {
|
||
const sig = formatMethodSignature(entry);
|
||
lines.push(` - \`${entry.name}${sig}\``);
|
||
}
|
||
}
|
||
|
||
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)`);
|
||
}
|