Compare commits

...

2 Commits

Author SHA1 Message Date
hypercross 73d49cb954 chore: bump version to 0.2.0 2026-04-14 23:58:36 +08:00
hypercross f5989a162b refactor: emit irprogram instead of yarndocument 2026-04-14 23:54:04 +08:00
12 changed files with 44 additions and 113 deletions

View File

@ -26,7 +26,7 @@ import { loadYarnProject } from 'yarn-spinner-loader';
const result = await loadYarnProject('path/to/project.yarnproject'); const result = await loadYarnProject('path/to/project.yarnproject');
console.log(result.project.projectName); console.log(result.project.projectName);
console.log(result.yarnFiles); // Array of parsed Yarn documents console.log(result.program); // Merged IRProgram from all .yarn files
``` ```
### With esbuild ### With esbuild
@ -103,11 +103,7 @@ Load and compile a `.yarnproject` file and all its referenced `.yarn` files.
interface LoadResult { interface LoadResult {
project: YarnProject; project: YarnProject;
baseDir: string; baseDir: string;
yarnFiles: Array<{ program: IRProgram;
relativePath: string;
absolutePath: string;
document: YarnDocument;
}>;
} }
``` ```

View File

@ -1,6 +1,6 @@
{ {
"name": "yarn-spinner-loader", "name": "yarn-spinner-loader",
"version": "0.1.1", "version": "0.2.0",
"description": "Load and compile Yarn Spinner project files for various build tools", "description": "Load and compile Yarn Spinner project files for various build tools",
"type": "module", "type": "module",
"main": "./dist/index.js", "main": "./dist/index.js",

View File

@ -14,7 +14,7 @@ export { YarnRunner } from './yarn-spinner/runtime/runner';
export type { export type {
LoadOptions, LoadOptions,
LoadResult, LoadResult,
LoadedYarnFile, IRProgram,
YarnProject, YarnProject,
SchemaValidationError, SchemaValidationError,
RunnerOptions, RunnerOptions,

View File

@ -15,7 +15,7 @@ export { YarnRunner } from './yarn-spinner/runtime/runner';
export type { export type {
LoadOptions, LoadOptions,
LoadResult, LoadResult,
LoadedYarnFile, IRProgram,
YarnProject, YarnProject,
SchemaValidationError, SchemaValidationError,
RunnerOptions, RunnerOptions,

View File

@ -1,10 +1,11 @@
import { readFileSync, existsSync } from 'node:fs'; import { readFileSync, existsSync } from 'node:fs';
import { dirname, resolve, relative } from 'node:path'; import { dirname, resolve } from 'node:path';
import fg from 'fast-glob'; import fg from 'fast-glob';
import type { YarnProject } from './types'; import type { YarnProject } from './types';
import { validateYarnProject, type SchemaValidationError } from './validator'; import { validateYarnProject, type SchemaValidationError } from './validator';
import { parseYarn } from '../yarn-spinner/parse/parser'; import { parseYarn } from '../yarn-spinner/parse/parser';
import type { YarnDocument } from '../yarn-spinner/model/ast'; import { compile } from '../yarn-spinner/compile/compiler';
import type { IRProgram } from '../yarn-spinner/compile/ir';
/** /**
* Options for loading a Yarn project * Options for loading a Yarn project
@ -14,18 +15,6 @@ export interface LoadOptions {
baseDir?: string; 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 * Result of loading a Yarn project
*/ */
@ -34,8 +23,8 @@ export interface LoadResult {
project: YarnProject; project: YarnProject;
/** Directory containing the .yarnproject file */ /** Directory containing the .yarnproject file */
baseDir: string; baseDir: string;
/** All loaded and parsed .yarn files */ /** Merged IR program from all .yarn files */
yarnFiles: LoadedYarnFile[]; program: IRProgram;
} }
/** /**
@ -71,7 +60,6 @@ export async function loadYarnProject(
projectFilePath: string, projectFilePath: string,
options: LoadOptions = {}, options: LoadOptions = {},
): Promise<LoadResult> { ): Promise<LoadResult> {
// Resolve and read .yarnproject file
const absoluteProjectPath = resolve(projectFilePath); const absoluteProjectPath = resolve(projectFilePath);
if (!existsSync(absoluteProjectPath)) { if (!existsSync(absoluteProjectPath)) {
@ -87,7 +75,6 @@ export async function loadYarnProject(
throw new LoadError(`Failed to parse .yarnproject file as JSON: ${absoluteProjectPath}`, error); throw new LoadError(`Failed to parse .yarnproject file as JSON: ${absoluteProjectPath}`, error);
} }
// Validate against schema
const validation = validateYarnProject(projectConfig); const validation = validateYarnProject(projectConfig);
if (!validation.valid) { if (!validation.valid) {
@ -100,7 +87,6 @@ export async function loadYarnProject(
const project = validation.data as YarnProject; const project = validation.data as YarnProject;
const baseDir = options.baseDir || dirname(absoluteProjectPath); const baseDir = options.baseDir || dirname(absoluteProjectPath);
// Find all .yarn files using glob patterns
const sourcePatterns = project.sourceFiles; const sourcePatterns = project.sourceFiles;
const ignorePatterns = project.excludeFiles || []; const ignorePatterns = project.excludeFiles || [];
@ -111,23 +97,21 @@ export async function loadYarnProject(
onlyFiles: true, onlyFiles: true,
}); });
// Load and parse each .yarn file const program: IRProgram = { enums: {}, nodes: {} };
const yarnFiles: LoadedYarnFile[] = yarnFilePaths.map(absolutePath => {
const relativePath = relative(baseDir, absolutePath); for (const absolutePath of yarnFilePaths) {
const content = readFileSync(absolutePath, 'utf-8'); const content = readFileSync(absolutePath, 'utf-8');
const document = parseYarn(content); const document = parseYarn(content);
const compiled = compile(document);
return { Object.assign(program.enums, compiled.enums);
relativePath, Object.assign(program.nodes, compiled.nodes);
absolutePath, }
document,
};
});
return { return {
project, project,
baseDir, baseDir,
yarnFiles, program,
}; };
} }
@ -138,7 +122,6 @@ export function loadYarnProjectSync(
projectFilePath: string, projectFilePath: string,
options: LoadOptions = {}, options: LoadOptions = {},
): LoadResult { ): LoadResult {
// Resolve and read .yarnproject file
const absoluteProjectPath = resolve(projectFilePath); const absoluteProjectPath = resolve(projectFilePath);
if (!existsSync(absoluteProjectPath)) { if (!existsSync(absoluteProjectPath)) {
@ -154,7 +137,6 @@ export function loadYarnProjectSync(
throw new LoadError(`Failed to parse .yarnproject file as JSON: ${absoluteProjectPath}`, error); throw new LoadError(`Failed to parse .yarnproject file as JSON: ${absoluteProjectPath}`, error);
} }
// Validate against schema
const validation = validateYarnProject(projectConfig); const validation = validateYarnProject(projectConfig);
if (!validation.valid) { if (!validation.valid) {
@ -167,7 +149,6 @@ export function loadYarnProjectSync(
const project = validation.data as YarnProject; const project = validation.data as YarnProject;
const baseDir = options.baseDir || dirname(absoluteProjectPath); const baseDir = options.baseDir || dirname(absoluteProjectPath);
// Find all .yarn files using glob patterns
const sourcePatterns = project.sourceFiles; const sourcePatterns = project.sourceFiles;
const ignorePatterns = project.excludeFiles || []; const ignorePatterns = project.excludeFiles || [];
@ -178,22 +159,20 @@ export function loadYarnProjectSync(
onlyFiles: true, onlyFiles: true,
}); });
// Load and parse each .yarn file const program: IRProgram = { enums: {}, nodes: {} };
const yarnFiles: LoadedYarnFile[] = yarnFilePaths.map(absolutePath => {
const relativePath = relative(baseDir, absolutePath); for (const absolutePath of yarnFilePaths) {
const content = readFileSync(absolutePath, 'utf-8'); const content = readFileSync(absolutePath, 'utf-8');
const document = parseYarn(content); const document = parseYarn(content);
const compiled = compile(document);
return { Object.assign(program.enums, compiled.enums);
relativePath, Object.assign(program.nodes, compiled.nodes);
absolutePath, }
document,
};
});
return { return {
project, project,
baseDir, baseDir,
yarnFiles, program,
}; };
} }

View File

@ -10,7 +10,6 @@ export {
loadYarnProjectSync, loadYarnProjectSync,
type LoadOptions, type LoadOptions,
type LoadResult, type LoadResult,
type LoadedYarnFile,
LoadError, LoadError,
ValidationError as LoaderValidationError, ValidationError as LoaderValidationError,
} from './loader/index'; } from './loader/index';

View File

@ -6,7 +6,7 @@
import type { YarnProject } from './loader/types'; import type { YarnProject } from './loader/types';
import type { SchemaValidationError } from './loader/validator'; import type { SchemaValidationError } from './loader/validator';
import type { YarnDocument } from './yarn-spinner/model/ast'; import type { IRProgram } from './yarn-spinner/compile/ir';
import type { RunnerOptions } from './yarn-spinner/runtime/runner'; import type { RunnerOptions } from './yarn-spinner/runtime/runner';
import type { import type {
TextResult, TextResult,
@ -18,7 +18,7 @@ import type {
export type { export type {
YarnProject, YarnProject,
SchemaValidationError, SchemaValidationError,
YarnDocument, IRProgram,
RunnerOptions, RunnerOptions,
TextResult, TextResult,
OptionsResult, OptionsResult,
@ -34,18 +34,6 @@ export interface LoadOptions {
baseDir?: string; 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 * Result of loading a Yarn project
*/ */
@ -54,6 +42,6 @@ export interface LoadResult {
project: YarnProject; project: YarnProject;
/** Directory containing the .yarnproject file */ /** Directory containing the .yarnproject file */
baseDir: string; baseDir: string;
/** All loaded and parsed .yarn files */ /** Merged IR program from all .yarn files */
yarnFiles: LoadedYarnFile[]; program: IRProgram;
} }

View File

@ -14,14 +14,14 @@ describe('loadYarnProject', () => {
expect(result.project.projectName).toBe('Simple Test Project'); expect(result.project.projectName).toBe('Simple Test Project');
expect(result.project.sourceFiles).toEqual(['**/*.yarn']); expect(result.project.sourceFiles).toEqual(['**/*.yarn']);
expect(result.project.baseLanguage).toBe('en'); expect(result.project.baseLanguage).toBe('en');
expect(result.yarnFiles.length).toBeGreaterThan(0); expect(Object.keys(result.program.nodes).length).toBeGreaterThan(0);
}); });
it('should parse all yarn files in simple project', async () => { it('should compile all yarn files in simple project', async () => {
const projectPath = resolve(__dirname, 'fixtures/simple/.yarnproject'); const projectPath = resolve(__dirname, 'fixtures/simple/.yarnproject');
const result = await loadYarnProject(projectPath); const result = await loadYarnProject(projectPath);
const titles = result.yarnFiles.flatMap(f => f.document.nodes.map(n => n.title)); const titles = Object.keys(result.program.nodes);
expect(titles).toContain('Start'); expect(titles).toContain('Start');
expect(titles).toContain('Greeting'); expect(titles).toContain('Greeting');
expect(titles).toContain('AnotherNode'); expect(titles).toContain('AnotherNode');
@ -35,7 +35,7 @@ describe('loadYarnProject', () => {
expect(result.project.baseLanguage).toBe('en'); expect(result.project.baseLanguage).toBe('en');
expect(result.project.localisation).toBeDefined(); expect(result.project.localisation).toBeDefined();
expect(result.project.localisation!['zh-Hans']).toBeDefined(); expect(result.project.localisation!['zh-Hans']).toBeDefined();
expect(result.yarnFiles.length).toBeGreaterThan(0); expect(Object.keys(result.program.nodes).length).toBeGreaterThan(0);
}); });
it('should load a complex project with multiple patterns', async () => { it('should load a complex project with multiple patterns', async () => {
@ -48,51 +48,21 @@ describe('loadYarnProject', () => {
expect(result.project.sourceFiles).toContain('scripts/**/*.yarn'); expect(result.project.sourceFiles).toContain('scripts/**/*.yarn');
}); });
it('should exclude files matching excludeFiles patterns', async () => {
const projectPath = resolve(__dirname, 'fixtures/complex/.yarnproject');
const result = await loadYarnProject(projectPath);
// Check that backup file is excluded
const backupFiles = result.yarnFiles.filter(f =>
f.relativePath.includes('backup'),
);
expect(backupFiles).toHaveLength(0);
});
it('should include files from multiple source patterns', async () => {
const projectPath = resolve(__dirname, 'fixtures/complex/.yarnproject');
const result = await loadYarnProject(projectPath);
const relativePaths = result.yarnFiles.map(f => f.relativePath);
// Should include dialogue files
expect(relativePaths.some(p => p.includes('dialogue'))).toBe(true);
// Should include script files
expect(relativePaths.some(p => p.includes('scripts'))).toBe(true);
});
it('should parse yarn documents correctly', async () => { it('should parse yarn documents correctly', async () => {
const projectPath = resolve(__dirname, 'fixtures/simple/.yarnproject'); const projectPath = resolve(__dirname, 'fixtures/simple/.yarnproject');
const result = await loadYarnProject(projectPath); const result = await loadYarnProject(projectPath);
const startNode = result.yarnFiles const startNode = result.program.nodes['Start'];
.flatMap(f => f.document.nodes)
.find(n => n.title === 'Start');
expect(startNode).toBeDefined(); expect(startNode).toBeDefined();
expect(startNode!.body.length).toBeGreaterThan(0); expect(startNode && 'instructions' in startNode ? startNode.instructions.length : 0).toBeGreaterThan(0);
}); });
it('should handle nodes with tags', async () => { it('should handle nodes with tags', async () => {
const projectPath = resolve(__dirname, 'fixtures/simple/.yarnproject'); const projectPath = resolve(__dirname, 'fixtures/simple/.yarnproject');
const result = await loadYarnProject(projectPath); const result = await loadYarnProject(projectPath);
const greetingNode = result.yarnFiles const greetingNode = result.program.nodes['Greeting'];
.flatMap(f => f.document.nodes)
.find(n => n.title === 'Greeting');
expect(greetingNode).toBeDefined(); expect(greetingNode).toBeDefined();
expect(greetingNode!.nodeTags).toContain('greeting');
}); });
it('should throw LoadError for non-existent file', async () => { it('should throw LoadError for non-existent file', async () => {
@ -104,7 +74,6 @@ describe('loadYarnProject', () => {
it('should throw error for invalid JSON', async () => { it('should throw error for invalid JSON', async () => {
const projectPath = resolve(__dirname, 'fixtures/invalid/.yarnproject'); const projectPath = resolve(__dirname, 'fixtures/invalid/.yarnproject');
// This will fail because the fixture doesn't exist
await expect(loadYarnProject(projectPath)).rejects.toThrow(); await expect(loadYarnProject(projectPath)).rejects.toThrow();
}); });
}); });
@ -115,14 +84,14 @@ describe('loadYarnProjectSync', () => {
const result = loadYarnProjectSync(projectPath); const result = loadYarnProjectSync(projectPath);
expect(result.project.projectFileVersion).toBe(4); expect(result.project.projectFileVersion).toBe(4);
expect(result.yarnFiles.length).toBeGreaterThan(0); expect(Object.keys(result.program.nodes).length).toBeGreaterThan(0);
}); });
it('should parse all yarn files synchronously', () => { it('should compile all yarn files synchronously', () => {
const projectPath = resolve(__dirname, 'fixtures/simple/.yarnproject'); const projectPath = resolve(__dirname, 'fixtures/simple/.yarnproject');
const result = loadYarnProjectSync(projectPath); const result = loadYarnProjectSync(projectPath);
const titles = result.yarnFiles.flatMap(f => f.document.nodes.map(n => n.title)); const titles = Object.keys(result.program.nodes);
expect(titles).toContain('Start'); expect(titles).toContain('Start');
}); });
}); });

View File

@ -60,7 +60,7 @@ describe('yarnSpinnerPlugin (esbuild)', () => {
expect(result).toBeDefined(); expect(result).toBeDefined();
expect(result.contents).toContain('export default'); expect(result.contents).toContain('export default');
expect(result.contents).toContain('project'); expect(result.contents).toContain('project');
expect(result.contents).toContain('yarnFiles'); expect(result.contents).toContain('program');
expect(result.loader).toBe('js'); expect(result.loader).toBe('js');
}); });

View File

@ -39,7 +39,7 @@ describe('yarnSpinnerRollup', () => {
expect(typeof result).toBe('string'); expect(typeof result).toBe('string');
expect(result).toContain('export default'); expect(result).toContain('export default');
expect(result).toContain('project'); expect(result).toContain('project');
expect(result).toContain('yarnFiles'); expect(result).toContain('program');
}); });
it('should return null for non-.yarnproject files', async () => { it('should return null for non-.yarnproject files', async () => {

View File

@ -39,7 +39,7 @@ describe('yarnSpinnerVite', () => {
expect(typeof result).toBe('string'); expect(typeof result).toBe('string');
expect(result).toContain('export default'); expect(result).toContain('export default');
expect(result).toContain('project'); expect(result).toContain('project');
expect(result).toContain('yarnFiles'); expect(result).toContain('program');
}); });
it('should return null for non-.yarnproject files', async () => { it('should return null for non-.yarnproject files', async () => {

View File

@ -26,7 +26,7 @@ describe('YarnSpinnerWebpackLoader', () => {
expect(typeof result).toBe('string'); expect(typeof result).toBe('string');
expect(result).toContain('export default'); expect(result).toContain('export default');
expect(result).toContain('project'); expect(result).toContain('project');
expect(result).toContain('yarnFiles'); expect(result).toContain('program');
// Ensure no errors were emitted // Ensure no errors were emitted
expect(mockContext.emitError).not.toHaveBeenCalled(); expect(mockContext.emitError).not.toHaveBeenCalled();
}); });