/** * Walks a folder of authored templates and lints each file. * * The Node adapter for the rules in `@beehexa/hexasync-template-assets`: the walking and YAML parsing * live here, the rules live in core, so the editor's Phase 1 diagnostics can apply the same * ones without reimplementing them. */ import { readFile, readdir } from 'node:fs/promises'; import { join, basename } from 'node:path'; import { parse } from 'yaml'; import { lintDocument, summariseLint, type LintSummary, } from '@beehexa/hexasync-template-assets'; /** Directories never authored by hand, so never linted. */ const SKIP_DIRS = new Set(['__configs', 'node_modules', '.git', '.hexasync']); async function* walk(dir: string): AsyncGenerator { for (const entry of await readdir(dir, { withFileTypes: true })) { if (entry.isDirectory()) { if (SKIP_DIRS.has(entry.name)) continue; yield* walk(join(dir, entry.name)); } else if (entry.name.endsWith('.yaml')) { // Composed output is generated, so its aggregation is not an authoring mistake. if (/^output.*\.yaml$/.test(entry.name)) continue; yield join(dir, entry.name); } } } export async function lintTree(root: string): Promise { const results: { path: string; diagnostics: ReturnType; }[] = []; for await (const file of walk(root)) { let document: unknown; try { document = parse(await readFile(file, 'utf8')); } catch { // A file that does not parse is a different problem, reported by compose rather than by a // structural lint. Skipping it here keeps this rule set about structure. continue; } const relative = file.startsWith(root) ? file.slice(root.length).replace(/^[/\\]/, '') : file; results.push({ path: relative, diagnostics: lintDocument(basename(file), document), }); } // Re-key the diagnostics onto the relative path, so a report points at the file a developer // can open rather than at a bare basename. return summariseLint( results.map((r) => ({ path: r.path, diagnostics: r.diagnostics.map((d) => ({ ...d, path: r.path })), })), ); }