import { readFileSync, existsSync } from 'node:fs'; import { dirname, resolve } from 'node:path'; import fg from 'fast-glob'; import type { YarnProject } from './types'; import { validateYarnProject, type SchemaValidationError } from './validator'; import { parseYarn } from '../yarn-spinner/parse/parser'; import { compile } from '../yarn-spinner/compile/compiler'; import type { IRProgram } from '../yarn-spinner/compile/ir'; /** * Options for loading a Yarn project */ export interface LoadOptions { /** Base directory for resolving glob patterns (default: directory of .yarnproject file) */ baseDir?: string; } /** * Result of loading a Yarn project */ export interface LoadResult { /** Parsed .yarnproject configuration */ project: YarnProject; /** Directory containing the .yarnproject file */ baseDir: string; /** Merged IR program from all .yarn files */ program: IRProgram; } /** * Error thrown when loading fails */ export class LoadError extends Error { constructor( message: string, public readonly cause?: unknown, ) { super(message); this.name = 'LoadError'; } } /** * Error thrown when validation fails */ export class ValidationError extends Error { constructor( message: string, public readonly errors: SchemaValidationError[], ) { super(message); this.name = 'ValidationError'; } } /** * Load and compile a .yarnproject file and all its referenced .yarn files */ export async function loadYarnProject( projectFilePath: string, options: LoadOptions = {}, ): Promise { const absoluteProjectPath = resolve(projectFilePath); if (!existsSync(absoluteProjectPath)) { throw new LoadError(`Project file not found: ${absoluteProjectPath}`); } const projectContent = readFileSync(absoluteProjectPath, 'utf-8'); let projectConfig: unknown; try { projectConfig = JSON.parse(projectContent); } catch (error) { throw new LoadError(`Failed to parse .yarnproject file as JSON: ${absoluteProjectPath}`, error); } const validation = validateYarnProject(projectConfig); if (!validation.valid) { throw new ValidationError( `Invalid .yarnproject file: ${validation.errors.map(e => `${e.path}: ${e.message}`).join(', ')}`, validation.errors, ); } const project = validation.data as YarnProject; const baseDir = options.baseDir || dirname(absoluteProjectPath); const sourcePatterns = project.sourceFiles; const ignorePatterns = project.excludeFiles || []; const yarnFilePaths = await fg.glob(sourcePatterns, { cwd: baseDir, ignore: ignorePatterns, absolute: true, onlyFiles: true, }); const program: IRProgram = { enums: {}, nodes: {} }; for (const absolutePath of yarnFilePaths) { const content = readFileSync(absolutePath, 'utf-8'); const document = parseYarn(content); const compiled = compile(document); Object.assign(program.enums, compiled.enums); Object.assign(program.nodes, compiled.nodes); } return { project, baseDir, program, }; } /** * Synchronous version of loadYarnProject */ export function loadYarnProjectSync( projectFilePath: string, options: LoadOptions = {}, ): LoadResult { const absoluteProjectPath = resolve(projectFilePath); if (!existsSync(absoluteProjectPath)) { throw new LoadError(`Project file not found: ${absoluteProjectPath}`); } const projectContent = readFileSync(absoluteProjectPath, 'utf-8'); let projectConfig: unknown; try { projectConfig = JSON.parse(projectContent); } catch (error) { throw new LoadError(`Failed to parse .yarnproject file as JSON: ${absoluteProjectPath}`, error); } const validation = validateYarnProject(projectConfig); if (!validation.valid) { throw new ValidationError( `Invalid .yarnproject file: ${validation.errors.map(e => `${e.path}: ${e.message}`).join(', ')}`, validation.errors, ); } const project = validation.data as YarnProject; const baseDir = options.baseDir || dirname(absoluteProjectPath); const sourcePatterns = project.sourceFiles; const ignorePatterns = project.excludeFiles || []; const yarnFilePaths = fg.globSync(sourcePatterns, { cwd: baseDir, ignore: ignorePatterns, absolute: true, onlyFiles: true, }); const program: IRProgram = { enums: {}, nodes: {} }; for (const absolutePath of yarnFilePaths) { const content = readFileSync(absolutePath, 'utf-8'); const document = parseYarn(content); const compiled = compile(document); Object.assign(program.enums, compiled.enums); Object.assign(program.nodes, compiled.nodes); } return { project, baseDir, program, }; }