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

179 lines
4.7 KiB
TypeScript

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