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', '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', '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, 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>} */ 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.
.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)`); }