import { readFileSync, existsSync } from 'node:fs'; import { dirname, resolve, relative } from 'node:path'; import { glob } from 'fast-glob'; import type { YarnProject } from './types'; import { validateYarnProject, type SchemaValidationError } from './validator'; import { parseYarn } from '../yarn-spinner/parse/parser'; import type { YarnDocument } from '../yarn-spinner/model/ast'; /** * Options for loading a Yarn project */ export interface LoadOptions { /** Base directory for resolving glob patterns (default: directory of .yarnproject file) */ baseDir?: string; } /** * Loaded Yarn document with metadata */ export interface LoadedYarnFile { /** File path relative to base directory */ relativePath: string; /** Absolute file path */ absolutePath: string; /** Parsed Yarn document */ document: YarnDocument; } /** * Result of loading a Yarn project */ export interface LoadResult { /** Parsed .yarnproject configuration */ project: YarnProject; /** Directory containing the .yarnproject file */ baseDir: string; /** All loaded and parsed .yarn files */ yarnFiles: LoadedYarnFile[]; } /** * 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 { // Resolve and read .yarnproject file 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); } // Validate against schema 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); // Find all .yarn files using glob patterns const sourcePatterns = project.sourceFiles; const ignorePatterns = project.excludeFiles || []; const yarnFilePaths = await glob(sourcePatterns, { cwd: baseDir, ignore: ignorePatterns, absolute: true, onlyFiles: true, }); // Load and parse each .yarn file const yarnFiles: LoadedYarnFile[] = yarnFilePaths.map(absolutePath => { const relativePath = relative(baseDir, absolutePath); const content = readFileSync(absolutePath, 'utf-8'); const document = parseYarn(content); return { relativePath, absolutePath, document, }; }); return { project, baseDir, yarnFiles, }; } /** * Synchronous version of loadYarnProject */ export function loadYarnProjectSync( projectFilePath: string, options: LoadOptions = {}, ): LoadResult { // Resolve and read .yarnproject file 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); } // Validate against schema 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); // Find all .yarn files using glob patterns const sourcePatterns = project.sourceFiles; const ignorePatterns = project.excludeFiles || []; const yarnFilePaths = glob.sync(sourcePatterns, { cwd: baseDir, ignore: ignorePatterns, absolute: true, onlyFiles: true, }); // Load and parse each .yarn file const yarnFiles: LoadedYarnFile[] = yarnFilePaths.map(absolutePath => { const relativePath = relative(baseDir, absolutePath); const content = readFileSync(absolutePath, 'utf-8'); const document = parseYarn(content); return { relativePath, absolutePath, document, }; }); return { project, baseDir, yarnFiles, }; }