179 lines
4.7 KiB
TypeScript
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,
|
|
};
|
|
}
|