blob: 8f5462e8749db47264701c953fd74dc5db476e47 [file]
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const matter = require('gray-matter');
const {
ensureDirForFile,
normalizePath,
parseArgs,
stripMarkdownExtension,
walkMarkdownFiles,
} = require('../docs-governance/lib');
const DOCS_DIR = 'docs/key-features';
const DOCS_ROUTE_BASE = '/docs/dev';
const OUTPUT_FILE = 'src/generated/key-features.ts';
function slugToRoute(slug, fallbackSlug) {
const raw = typeof slug === 'string' && slug.trim() ? slug.trim() : fallbackSlug;
const normalized = raw.replace(/^\/+/, '/').replace(/\/+$/, '');
return normalized === '/' ? '/' : normalized;
}
function resolveHref(frontMatter, docsRelativePath) {
const cardHref = frontMatter?.featureCard?.href;
if (typeof cardHref === 'string' && cardHref.trim()) {
return cardHref.trim();
}
const fallbackSlug = `/${stripMarkdownExtension(docsRelativePath)}`;
const routeSlug = slugToRoute(frontMatter.slug, fallbackSlug);
return `${DOCS_ROUTE_BASE}${routeSlug}`;
}
function normalizeTags(value, sourcePath) {
if (!Array.isArray(value) || value.length === 0) {
throw new Error(`Missing featureCard.tags in ${sourcePath}`);
}
return value.map((tag) => {
if (typeof tag !== 'string' || !tag.trim()) {
throw new Error(`Invalid featureCard.tags entry in ${sourcePath}`);
}
return tag.trim();
});
}
function parseFeatureDoc(rootDir, absPath, index) {
const raw = fs.readFileSync(absPath, 'utf8');
const parsed = matter(raw);
const data = parsed.data || {};
const featureCard = data.featureCard || {};
const relativePath = normalizePath(path.relative(rootDir, absPath));
const docsRelativePath = normalizePath(path.relative(path.join(rootDir, 'docs'), absPath));
const sourcePath = relativePath;
const title = typeof data.title === 'string' ? data.title.trim() : '';
const description = typeof data.description === 'string' ? data.description.trim() : '';
if (!title) {
throw new Error(`Missing required front matter field "title" in ${sourcePath}`);
}
if (!description) {
throw new Error(`Missing required front matter field "description" in ${sourcePath}`);
}
const href = resolveHref(data, docsRelativePath);
const badge =
typeof featureCard.badge === 'string' && featureCard.badge.trim()
? featureCard.badge.trim()
: typeof featureCard.href === 'string' && featureCard.href.trim()
? 'doc'
: undefined;
const id = stripMarkdownExtension(docsRelativePath).replace(/\//g, '-');
return {
id,
title,
description,
tags: normalizeTags(featureCard.tags, sourcePath),
href,
...(badge ? { badge } : {}),
sourcePath,
};
}
function generate({ rootDir = process.cwd(), output = OUTPUT_FILE } = {}) {
const absDocsDir = path.join(rootDir, DOCS_DIR);
if (!fs.existsSync(absDocsDir)) {
throw new Error(`Missing docs directory: ${DOCS_DIR}`);
}
const files = walkMarkdownFiles(rootDir, [DOCS_DIR]);
const cards = files.map((absPath, index) => parseFeatureDoc(rootDir, absPath, index));
cards.sort((a, b) => a.title.localeCompare(b.title) || a.href.localeCompare(b.href));
const seenIds = new Set();
const seenHref = new Set();
for (const card of cards) {
if (seenIds.has(card.id)) {
throw new Error(`Duplicate key-features id: ${card.id}`);
}
if (seenHref.has(card.href)) {
throw new Error(`Duplicate key-features href: ${card.href}`);
}
seenIds.add(card.id);
seenHref.add(card.href);
}
const outputPath = path.resolve(rootDir, output);
ensureDirForFile(outputPath);
const file = `/* AUTO-GENERATED FILE. DO NOT EDIT DIRECTLY. */
export type KeyFeatureCard = {
id: string;
title: string;
description: string;
tags: string[];
href: string;
badge?: string;
};
export const keyFeatureCards: KeyFeatureCard[] = ${JSON.stringify(
cards.map(({ sourcePath, ...card }) => card),
null,
2,
)};
`;
fs.writeFileSync(outputPath, `${file}\n`, 'utf8');
return cards;
}
function runCli() {
const args = parseArgs(process.argv.slice(2));
const rootDir = args.root ? path.resolve(process.cwd(), args.root) : process.cwd();
const output = args.output || OUTPUT_FILE;
generate({ rootDir, output });
}
if (require.main === module) {
runCli();
}
module.exports = {
generate,
};