yarn-spinner-loader/src/loader/index.ts

200 lines
5.1 KiB
TypeScript

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<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 = 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,
};
}