diff --git a/README.md b/README.md index 4e13e3a..2241ef0 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ import { loadYarnProject } from 'yarn-spinner-loader'; const result = await loadYarnProject('path/to/project.yarnproject'); 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 @@ -103,11 +103,7 @@ Load and compile a `.yarnproject` file and all its referenced `.yarn` files. interface LoadResult { project: YarnProject; baseDir: string; - yarnFiles: Array<{ - relativePath: string; - absolutePath: string; - document: YarnDocument; - }>; + program: IRProgram; } ``` diff --git a/src/core.ts b/src/core.ts index 9b68464..5042ee7 100644 --- a/src/core.ts +++ b/src/core.ts @@ -14,7 +14,7 @@ export { YarnRunner } from './yarn-spinner/runtime/runner'; export type { LoadOptions, LoadResult, - LoadedYarnFile, + IRProgram, YarnProject, SchemaValidationError, RunnerOptions, diff --git a/src/index.ts b/src/index.ts index c7c919c..b5ed58f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,7 +15,7 @@ export { YarnRunner } from './yarn-spinner/runtime/runner'; export type { LoadOptions, LoadResult, - LoadedYarnFile, + IRProgram, YarnProject, SchemaValidationError, RunnerOptions, diff --git a/src/loader/index.ts b/src/loader/index.ts index 3eded0a..c2ee19f 100644 --- a/src/loader/index.ts +++ b/src/loader/index.ts @@ -1,10 +1,11 @@ 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 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'; +import { compile } from '../yarn-spinner/compile/compiler'; +import type { IRProgram } from '../yarn-spinner/compile/ir'; /** * Options for loading a Yarn project @@ -14,18 +15,6 @@ export interface LoadOptions { 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 */ @@ -34,8 +23,8 @@ export interface LoadResult { project: YarnProject; /** Directory containing the .yarnproject file */ baseDir: string; - /** All loaded and parsed .yarn files */ - yarnFiles: LoadedYarnFile[]; + /** Merged IR program from all .yarn files */ + program: IRProgram; } /** @@ -71,7 +60,6 @@ export async function loadYarnProject( projectFilePath: string, options: LoadOptions = {}, ): Promise { - // Resolve and read .yarnproject file const absoluteProjectPath = resolve(projectFilePath); if (!existsSync(absoluteProjectPath)) { @@ -87,7 +75,6 @@ export async function loadYarnProject( throw new LoadError(`Failed to parse .yarnproject file as JSON: ${absoluteProjectPath}`, error); } - // Validate against schema const validation = validateYarnProject(projectConfig); if (!validation.valid) { @@ -100,7 +87,6 @@ export async function loadYarnProject( 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 || []; @@ -111,23 +97,21 @@ export async function loadYarnProject( onlyFiles: true, }); - // Load and parse each .yarn file - const yarnFiles: LoadedYarnFile[] = yarnFilePaths.map(absolutePath => { - const relativePath = relative(baseDir, absolutePath); + const program: IRProgram = { enums: {}, nodes: {} }; + + for (const absolutePath of yarnFilePaths) { const content = readFileSync(absolutePath, 'utf-8'); const document = parseYarn(content); + const compiled = compile(document); - return { - relativePath, - absolutePath, - document, - }; - }); + Object.assign(program.enums, compiled.enums); + Object.assign(program.nodes, compiled.nodes); + } return { project, baseDir, - yarnFiles, + program, }; } @@ -138,7 +122,6 @@ export function loadYarnProjectSync( projectFilePath: string, options: LoadOptions = {}, ): LoadResult { - // Resolve and read .yarnproject file const absoluteProjectPath = resolve(projectFilePath); if (!existsSync(absoluteProjectPath)) { @@ -154,7 +137,6 @@ export function loadYarnProjectSync( throw new LoadError(`Failed to parse .yarnproject file as JSON: ${absoluteProjectPath}`, error); } - // Validate against schema const validation = validateYarnProject(projectConfig); if (!validation.valid) { @@ -167,7 +149,6 @@ export function loadYarnProjectSync( 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 || []; @@ -178,22 +159,20 @@ export function loadYarnProjectSync( onlyFiles: true, }); - // Load and parse each .yarn file - const yarnFiles: LoadedYarnFile[] = yarnFilePaths.map(absolutePath => { - const relativePath = relative(baseDir, absolutePath); + const program: IRProgram = { enums: {}, nodes: {} }; + + for (const absolutePath of yarnFilePaths) { const content = readFileSync(absolutePath, 'utf-8'); const document = parseYarn(content); + const compiled = compile(document); - return { - relativePath, - absolutePath, - document, - }; - }); + Object.assign(program.enums, compiled.enums); + Object.assign(program.nodes, compiled.nodes); + } return { project, baseDir, - yarnFiles, + program, }; } diff --git a/src/node.ts b/src/node.ts index 7ee2a2c..2c8ec2d 100644 --- a/src/node.ts +++ b/src/node.ts @@ -10,7 +10,6 @@ export { loadYarnProjectSync, type LoadOptions, type LoadResult, - type LoadedYarnFile, LoadError, ValidationError as LoaderValidationError, } from './loader/index'; diff --git a/src/types.ts b/src/types.ts index 69e95ee..ac64608 100644 --- a/src/types.ts +++ b/src/types.ts @@ -6,7 +6,7 @@ import type { YarnProject } from './loader/types'; 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 { TextResult, @@ -18,7 +18,7 @@ import type { export type { YarnProject, SchemaValidationError, - YarnDocument, + IRProgram, RunnerOptions, TextResult, OptionsResult, @@ -34,18 +34,6 @@ export interface LoadOptions { 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 */ @@ -54,6 +42,6 @@ export interface LoadResult { project: YarnProject; /** Directory containing the .yarnproject file */ baseDir: string; - /** All loaded and parsed .yarn files */ - yarnFiles: LoadedYarnFile[]; + /** Merged IR program from all .yarn files */ + program: IRProgram; } diff --git a/tests/loader.test.ts b/tests/loader.test.ts index a952d54..6a68b75 100644 --- a/tests/loader.test.ts +++ b/tests/loader.test.ts @@ -14,14 +14,14 @@ describe('loadYarnProject', () => { expect(result.project.projectName).toBe('Simple Test Project'); expect(result.project.sourceFiles).toEqual(['**/*.yarn']); 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 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('Greeting'); expect(titles).toContain('AnotherNode'); @@ -35,7 +35,7 @@ describe('loadYarnProject', () => { expect(result.project.baseLanguage).toBe('en'); expect(result.project.localisation).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 () => { @@ -48,51 +48,21 @@ describe('loadYarnProject', () => { 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 () => { const projectPath = resolve(__dirname, 'fixtures/simple/.yarnproject'); const result = await loadYarnProject(projectPath); - const startNode = result.yarnFiles - .flatMap(f => f.document.nodes) - .find(n => n.title === 'Start'); - + const startNode = result.program.nodes['Start']; 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 () => { const projectPath = resolve(__dirname, 'fixtures/simple/.yarnproject'); const result = await loadYarnProject(projectPath); - const greetingNode = result.yarnFiles - .flatMap(f => f.document.nodes) - .find(n => n.title === 'Greeting'); - + const greetingNode = result.program.nodes['Greeting']; expect(greetingNode).toBeDefined(); - expect(greetingNode!.nodeTags).toContain('greeting'); }); it('should throw LoadError for non-existent file', async () => { @@ -104,7 +74,6 @@ describe('loadYarnProject', () => { it('should throw error for invalid JSON', async () => { const projectPath = resolve(__dirname, 'fixtures/invalid/.yarnproject'); - // This will fail because the fixture doesn't exist await expect(loadYarnProject(projectPath)).rejects.toThrow(); }); }); @@ -115,14 +84,14 @@ describe('loadYarnProjectSync', () => { const result = loadYarnProjectSync(projectPath); 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 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'); }); }); diff --git a/tests/plugins/esbuild.test.ts b/tests/plugins/esbuild.test.ts index 58d720b..dcb3211 100644 --- a/tests/plugins/esbuild.test.ts +++ b/tests/plugins/esbuild.test.ts @@ -60,7 +60,7 @@ describe('yarnSpinnerPlugin (esbuild)', () => { expect(result).toBeDefined(); expect(result.contents).toContain('export default'); expect(result.contents).toContain('project'); - expect(result.contents).toContain('yarnFiles'); + expect(result.contents).toContain('program'); expect(result.loader).toBe('js'); }); diff --git a/tests/plugins/rollup.test.ts b/tests/plugins/rollup.test.ts index 5597f71..7e9f749 100644 --- a/tests/plugins/rollup.test.ts +++ b/tests/plugins/rollup.test.ts @@ -39,7 +39,7 @@ describe('yarnSpinnerRollup', () => { expect(typeof result).toBe('string'); expect(result).toContain('export default'); expect(result).toContain('project'); - expect(result).toContain('yarnFiles'); + expect(result).toContain('program'); }); it('should return null for non-.yarnproject files', async () => { diff --git a/tests/plugins/vite.test.ts b/tests/plugins/vite.test.ts index ecc653c..fc9f2fe 100644 --- a/tests/plugins/vite.test.ts +++ b/tests/plugins/vite.test.ts @@ -39,7 +39,7 @@ describe('yarnSpinnerVite', () => { expect(typeof result).toBe('string'); expect(result).toContain('export default'); expect(result).toContain('project'); - expect(result).toContain('yarnFiles'); + expect(result).toContain('program'); }); it('should return null for non-.yarnproject files', async () => { diff --git a/tests/plugins/webpack.test.ts b/tests/plugins/webpack.test.ts index ad529e8..48331fd 100644 --- a/tests/plugins/webpack.test.ts +++ b/tests/plugins/webpack.test.ts @@ -26,7 +26,7 @@ describe('YarnSpinnerWebpackLoader', () => { expect(typeof result).toBe('string'); expect(result).toContain('export default'); expect(result).toContain('project'); - expect(result).toContain('yarnFiles'); + expect(result).toContain('program'); // Ensure no errors were emitted expect(mockContext.emitError).not.toHaveBeenCalled(); });