/** * Generic generation pipeline orchestration. * * Provides the step sequencing, logging, warning collection, stats counting, * and artifact writing pattern. Individual steps (fetch, parse, etc.) are * supplied by the lexicon via callbacks. */ import { writeFileSync, mkdirSync, existsSync } from "fs"; import { join } from "path"; import type { NamingStrategy } from "./naming"; // ── Types ────────────────────────────────────────────────────────── export interface GenerateOptions { force?: boolean; verbose?: boolean; dryRun?: boolean; schemaSource?: Map; } export interface GenerateResult { lexiconJSON: string; typesDTS: string; indexTS: string; resources: number; properties: number; enums: number; warnings: Array<{ file: string; error: string }>; /** * Additional generated files, keyed by filename, produced by the optional * {@link GeneratePipelineConfig.generateExtraArtifacts} hook. They come out * of the same parse a lexicon's types and registry come out of, which is the * point: an artifact derived here cannot drift from the types, the way a * hand-maintained table beside them can (chant #1074's operation surface is * the first of these). */ extraArtifacts?: Record; } /** * A parsed result with enough structure for the pipeline to count stats. * Lexicons extend this with their own fields. */ export interface ParsedResult { propertyTypes: Array<{ name: string }>; enums: Array; } export interface AugmentResult { schemas: Map; extraResults?: T[]; warnings?: Array<{ file: string; error: string }>; } export interface GeneratePipelineConfig { /** Fetch or provide raw schema data. */ fetchSchemas: (opts: { force?: boolean }) => Promise>; /** * Parse a single schema buffer into results. Returns null to skip. * * May return an array when a single schema file produces multiple results * (e.g. K8s OpenAPI spec, GitLab CI schema). */ parseSchema: (typeName: string, data: Buffer) => T | T[] | null; /** Create a naming strategy from the parsed results. */ createNaming: (results: T[]) => NamingStrategy; /** Generate lexicon JSON from results + naming. */ generateRegistry: (results: T[], naming: NamingStrategy) => string; /** Generate TypeScript declarations. */ generateTypes: (results: T[], naming: NamingStrategy) => string; /** Generate runtime index with factory exports. */ generateRuntimeIndex: (results: T[], naming: NamingStrategy) => string; /** * Optional extra artifacts from the same parsed results — filename → content. * Used when a lexicon needs a second derived table alongside the registry and * the types, and needs it to come from the same pass so the three cannot * skew. */ generateExtraArtifacts?: (results: T[], naming: NamingStrategy) => Record; /** Optional pre-parse hook (patches, overlays, extra resources, etc.). */ augmentSchemas?: ( schemas: Map, opts: GenerateOptions, log: (msg: string) => void, ) => Promise>; /** Optional post-parse hook (add synthetic resources, fallbacks, etc.). */ augmentResults?: ( results: T[], opts: GenerateOptions, log: (msg: string) => void, ) => { results: T[]; warnings?: Array<{ file: string; error: string }> }; } // ── Pipeline ─────────────────────────────────────────────────────── /** * Run a generation pipeline with the supplied config callbacks. */ export async function generatePipeline( config: GeneratePipelineConfig, opts: GenerateOptions = {}, ): Promise { const log = opts.verbose ? (msg: string) => console.error(msg) : (_msg: string) => {}; const warnings: Array<{ file: string; error: string }> = []; // Step 1: Fetch schemas (or use provided source) let schemas: Map; if (opts.schemaSource) { schemas = opts.schemaSource; log(`Using provided schema source with ${schemas.size} schemas`); } else { log("Fetching schemas..."); schemas = await config.fetchSchemas({ force: opts.force }); log(`Fetched ${schemas.size} schemas`); } // Step 2: Augment schemas (patches, overlays, etc.) let extraResults: T[] = []; if (config.augmentSchemas && !opts.schemaSource) { const augment = await config.augmentSchemas(schemas, opts, log); schemas = augment.schemas; if (augment.extraResults) extraResults = augment.extraResults; if (augment.warnings) warnings.push(...augment.warnings); } // Step 3: Parse each schema log("Parsing schemas..."); const results: T[] = []; for (const [typeName, data] of schemas) { try { const result = config.parseSchema(typeName, data); if (result) { if (Array.isArray(result)) { results.push(...result); } else { results.push(result); } } } catch (err) { warnings.push({ file: typeName, error: err instanceof Error ? err.message : String(err), }); } } results.push(...extraResults); log(`Parsed ${results.length} schemas`); // Step 4: Augment results (fallbacks, synthetic resources, etc.) if (config.augmentResults) { const augment = config.augmentResults(results, opts, log); // augmentResults may mutate results in-place or return new ones if (augment.warnings) warnings.push(...augment.warnings); } // Step 5: Naming strategy const naming = config.createNaming(results); // Step 6: Generate artifacts log("Generating lexicon JSON..."); const lexiconJSON = config.generateRegistry(results, naming); log("Generating TypeScript declarations..."); const typesDTS = config.generateTypes(results, naming); log("Generating runtime index..."); const indexTS = config.generateRuntimeIndex(results, naming); let extraArtifacts: Record | undefined; if (config.generateExtraArtifacts) { log("Generating extra artifacts..."); extraArtifacts = config.generateExtraArtifacts(results, naming); log(`Generated ${Object.keys(extraArtifacts).length} extra artifact(s)`); } // Count stats let resourceCount = 0; let propertyCount = 0; let enumCount = 0; for (const r of results) { resourceCount++; propertyCount += r.propertyTypes.length; enumCount += r.enums.length; } return { lexiconJSON, typesDTS, indexTS, resources: resourceCount, properties: propertyCount, enums: enumCount, warnings, ...(extraArtifacts ? { extraArtifacts } : {}), }; } // ── Artifact writing ─────────────────────────────────────────────── export interface WriteConfig { /** Base directory of the lexicon package. */ baseDir: string; /** Subdirectory for generated files (default: "src/generated"). */ generatedSubdir?: string; /** Map of filename → content to write. */ files: Record; } /** * Write generated artifacts to disk. */ export function writeGeneratedArtifacts(config: WriteConfig): void { const generatedDir = join(config.baseDir, config.generatedSubdir ?? "src/generated"); mkdirSync(generatedDir, { recursive: true }); for (const [filename, content] of Object.entries(config.files)) { writeFileSync(join(generatedDir, filename), content); } }