Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/lukebennett88/luke-ui. ui.lukebennett.dev
Something went wrong. Try again.
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';import { dirname, resolve } from 'node:path';import { fileURLToPath } from 'node:url';import { readFrontmatter } from '../src/lib/docs-frontmatter.ts';
const scriptDir = dirname(fileURLToPath(import.meta.url));const componentsDir = resolve(scriptDir, '../content/docs/components');const outputPath = resolve(scriptDir, '../src/generated/components-index.generated.ts');
interface ComponentIndexEntry { description: string; name: string; url: string;}
interface ComponentIndexGroup { entries: Array<ComponentIndexEntry>; title: string;}
/** * Emits the docs app's single runtime source of truth for the component catalogue: one entry per * component guide, grouped by category in sidebar order. The components landing page renders from * this array, so the documented set can never drift from the guides that live on disk. */export function generateComponentsIndex(rootDir: string = componentsDir): string { const groups = readGroups(rootDir);
const groupEntries = groups .map( (group) => `\t{\t\ttitle: ${quote(group.title)},\t\tentries: [${group.entries .map((entry) => { return `\t\t\t{ description: ${quote(entry.description)}, name: ${quote(entry.name)}, url: ${quote(entry.url)} },`; }) .join('\n')}\t\t],\t},`, ) .join('\n');
return `// Generated by scripts/generate-components-index.ts — do not edit.
/** One component guide: its title, one-line description, and URL. */export interface ComponentIndexEntry { description: string; name: string; url: string;}
/** A component category with its guides, in sidebar order. */export interface ComponentIndexGroup { entries: ReadonlyArray<ComponentIndexEntry>; title: string;}
/** Every component guide, grouped by category, in sidebar order. */export const componentIndexGroups: ReadonlyArray<ComponentIndexGroup> = [${groupEntries}];`;}
function readGroups(rootDir: string): ReadonlyArray<ComponentIndexGroup> { const rootMeta = readJson(resolve(rootDir, 'meta.json')) as { pages?: Array<string> }; const entries = rootMeta.pages ?? []; const groups: Array<ComponentIndexGroup> = []; let currentTitle = 'Components'; // Stays undefined until a source-backed guide arrives, so separators never emit an empty group. let currentGroup: ComponentIndexGroup | undefined;
for (const entry of entries) { if (isSeparator(entry)) { currentTitle = entry.slice(3, -3); currentGroup = undefined; continue; }
const guidePath = resolve(rootDir, `${entry}.mdx`); if (!existsSync(guidePath)) continue;
const frontmatter = readFrontmatter(readFileSync(guidePath, 'utf8')); if (frontmatter.source === undefined) continue;
if (!currentGroup) { currentGroup = { entries: [], title: currentTitle }; groups.push(currentGroup); }
currentGroup.entries.push({ description: frontmatter.description ?? '', name: frontmatter.title ?? entry, url: `/components/${entry}`, }); }
return groups;}
function isSeparator(value: string): boolean { return /^---.+---$/.test(value);}
function readJson(path: string): unknown { return JSON.parse(readFileSync(path, 'utf8'));}
function quote(text: string): string { return `'${text.replaceAll('\\', '\\\\').replaceAll("'", "\\'")}'`;}
if (process.argv[1] !== undefined && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { const output = generateComponentsIndex(); mkdirSync(dirname(outputPath), { recursive: true }); writeFileSync(outputPath, output); const entryCount = readGroups(componentsDir).flatMap((group) => group.entries).length; // oxlint-disable-next-line no-console console.log(`generate-components-index: wrote ${entryCount} entries`);}