feat: impl
This commit is contained in:
Vendored
+29
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"projectFileVersion": 4,
|
||||
"projectName": "Complex Test Project",
|
||||
"authorName": ["Author One", "Author Two"],
|
||||
"sourceFiles": [
|
||||
"dialogue/**/*.yarn",
|
||||
"scripts/**/*.yarn"
|
||||
],
|
||||
"excludeFiles": [
|
||||
"**/*.backup.yarn",
|
||||
"dialogue/deprecated/**/*.yarn"
|
||||
],
|
||||
"baseLanguage": "en",
|
||||
"localisation": {
|
||||
"zh-Hans": {
|
||||
"strings": "translations/zh-Hans.csv"
|
||||
}
|
||||
},
|
||||
"definitions": ["custom-commands.ysls.json"],
|
||||
"compilerOptions": {
|
||||
"requireVariableDeclarations": true,
|
||||
"allowPreviewFeatures": false
|
||||
},
|
||||
"editorOptions": {
|
||||
"yarnScriptEditor": {
|
||||
"theme": "dark"
|
||||
}
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
title: ComplexNode1
|
||||
tags: test complex
|
||||
when: $has_sword
|
||||
---
|
||||
Character: This is a complex node with tags and conditions.
|
||||
<<once>>
|
||||
This text only shows once.
|
||||
<<endonce>>
|
||||
===
|
||||
|
||||
title: ComplexNode2
|
||||
---
|
||||
<<if $has_key>>
|
||||
Character: You have the key!
|
||||
<<else>>
|
||||
Character: You need a key to proceed.
|
||||
<<endif>>
|
||||
|
||||
Here are your options:
|
||||
-> [Option 1] Go left
|
||||
You went left.
|
||||
-> [Option 2] Go right
|
||||
You went right.
|
||||
===
|
||||
@@ -0,0 +1,4 @@
|
||||
title: BackupNode
|
||||
---
|
||||
This file should be excluded.
|
||||
===
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
title: ScriptNode
|
||||
---
|
||||
<<set $variable = 1>>
|
||||
<<set $another = true>>
|
||||
===
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"projectFileVersion": 4,
|
||||
"projectName": "Localised Project",
|
||||
"sourceFiles": ["**/*.yarn"],
|
||||
"baseLanguage": "en",
|
||||
"localisation": {
|
||||
"zh-Hans": {
|
||||
"strings": "translations/zh-Hans.csv",
|
||||
"assets": "translations/zh-Hans/"
|
||||
},
|
||||
"ja": {
|
||||
"strings": "translations/ja.csv"
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
title: Welcome
|
||||
---
|
||||
Welcome to the localised project!
|
||||
This text should be translated.
|
||||
===
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"projectFileVersion": 4,
|
||||
"projectName": "Simple Test Project",
|
||||
"sourceFiles": ["**/*.yarn"],
|
||||
"baseLanguage": "en"
|
||||
}
|
||||
Vendored
+12
@@ -0,0 +1,12 @@
|
||||
title: Start
|
||||
---
|
||||
Hello, world!
|
||||
This is a simple test dialogue.
|
||||
===
|
||||
|
||||
title: Greeting
|
||||
tags: greeting
|
||||
---
|
||||
Player: Hi there!
|
||||
NPC: Welcome to our game!
|
||||
===
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
title: AnotherNode
|
||||
---
|
||||
This is another test node.
|
||||
<<if $variable>>
|
||||
Conditional text here.
|
||||
<<endif>>
|
||||
===
|
||||
@@ -0,0 +1,128 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { loadYarnProject, loadYarnProjectSync, LoadError } from '../src/loader/index';
|
||||
import { resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
||||
|
||||
describe('loadYarnProject', () => {
|
||||
it('should load a simple project', async () => {
|
||||
const projectPath = resolve(__dirname, 'fixtures/simple/.yarnproject');
|
||||
const result = await loadYarnProject(projectPath);
|
||||
|
||||
expect(result.project.projectFileVersion).toBe(4);
|
||||
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);
|
||||
});
|
||||
|
||||
it('should parse 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));
|
||||
expect(titles).toContain('Start');
|
||||
expect(titles).toContain('Greeting');
|
||||
expect(titles).toContain('AnotherNode');
|
||||
});
|
||||
|
||||
it('should load a localised project', async () => {
|
||||
const projectPath = resolve(__dirname, 'fixtures/localised/.yarnproject');
|
||||
const result = await loadYarnProject(projectPath);
|
||||
|
||||
expect(result.project.projectName).toBe('Localised Project');
|
||||
expect(result.project.baseLanguage).toBe('en');
|
||||
expect(result.project.localisation).toBeDefined();
|
||||
expect(result.project.localisation!['zh-Hans']).toBeDefined();
|
||||
expect(result.yarnFiles.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should load a complex project with multiple patterns', async () => {
|
||||
const projectPath = resolve(__dirname, 'fixtures/complex/.yarnproject');
|
||||
const result = await loadYarnProject(projectPath);
|
||||
|
||||
expect(result.project.projectFileVersion).toBe(4);
|
||||
expect(result.project.authorName).toEqual(['Author One', 'Author Two']);
|
||||
expect(result.project.sourceFiles).toContain('dialogue/**/*.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 () => {
|
||||
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');
|
||||
|
||||
expect(startNode).toBeDefined();
|
||||
expect(startNode!.body.length).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');
|
||||
|
||||
expect(greetingNode).toBeDefined();
|
||||
expect(greetingNode!.nodeTags).toContain('greeting');
|
||||
});
|
||||
|
||||
it('should throw LoadError for non-existent file', async () => {
|
||||
const projectPath = resolve(__dirname, 'fixtures/nonexistent/.yarnproject');
|
||||
|
||||
await expect(loadYarnProject(projectPath)).rejects.toThrow(LoadError);
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadYarnProjectSync', () => {
|
||||
it('should load a simple project synchronously', () => {
|
||||
const projectPath = resolve(__dirname, 'fixtures/simple/.yarnproject');
|
||||
const result = loadYarnProjectSync(projectPath);
|
||||
|
||||
expect(result.project.projectFileVersion).toBe(4);
|
||||
expect(result.yarnFiles.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should parse 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));
|
||||
expect(titles).toContain('Start');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,167 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { validateYarnProject, isYarnProject } from '../src/loader/validator';
|
||||
|
||||
describe('validateYarnProject', () => {
|
||||
it('should validate a minimal valid config', () => {
|
||||
const config = {
|
||||
projectFileVersion: 4,
|
||||
sourceFiles: ['**/*.yarn'],
|
||||
baseLanguage: 'en',
|
||||
};
|
||||
|
||||
const result = validateYarnProject(config);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
it('should validate a full config', () => {
|
||||
const config = {
|
||||
projectFileVersion: 4,
|
||||
projectName: 'Test Project',
|
||||
authorName: ['Author 1', 'Author 2'],
|
||||
sourceFiles: ['**/*.yarn'],
|
||||
excludeFiles: ['**/*.backup.yarn'],
|
||||
baseLanguage: 'en',
|
||||
localisation: {
|
||||
'zh-Hans': {
|
||||
strings: 'translations/zh-Hans.csv',
|
||||
assets: 'translations/zh-Hans/',
|
||||
},
|
||||
},
|
||||
definitions: ['custom.ysls.json'],
|
||||
compilerOptions: {
|
||||
requireVariableDeclarations: true,
|
||||
allowPreviewFeatures: false,
|
||||
},
|
||||
editorOptions: {
|
||||
yarnScriptEditor: { theme: 'dark' },
|
||||
},
|
||||
};
|
||||
|
||||
const result = validateYarnProject(config);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
it('should reject missing projectFileVersion', () => {
|
||||
const config = {
|
||||
sourceFiles: ['**/*.yarn'],
|
||||
baseLanguage: 'en',
|
||||
};
|
||||
|
||||
const result = validateYarnProject(config);
|
||||
expect(result.valid).toBe(false);
|
||||
if (!result.valid) {
|
||||
expect(result.errors.length).toBeGreaterThan(0);
|
||||
expect(result.errors[0].message).toContain('projectFileVersion');
|
||||
}
|
||||
});
|
||||
|
||||
it('should reject missing sourceFiles', () => {
|
||||
const config = {
|
||||
projectFileVersion: 4,
|
||||
baseLanguage: 'en',
|
||||
};
|
||||
|
||||
const result = validateYarnProject(config);
|
||||
expect(result.valid).toBe(false);
|
||||
});
|
||||
|
||||
it('should reject missing baseLanguage', () => {
|
||||
const config = {
|
||||
projectFileVersion: 4,
|
||||
sourceFiles: ['**/*.yarn'],
|
||||
};
|
||||
|
||||
const result = validateYarnProject(config);
|
||||
expect(result.valid).toBe(false);
|
||||
});
|
||||
|
||||
it('should reject invalid projectFileVersion type', () => {
|
||||
const config = {
|
||||
projectFileVersion: '4',
|
||||
sourceFiles: ['**/*.yarn'],
|
||||
baseLanguage: 'en',
|
||||
};
|
||||
|
||||
const result = validateYarnProject(config);
|
||||
expect(result.valid).toBe(false);
|
||||
});
|
||||
|
||||
it('should reject projectFileVersion less than 2', () => {
|
||||
const config = {
|
||||
projectFileVersion: 1,
|
||||
sourceFiles: ['**/*.yarn'],
|
||||
baseLanguage: 'en',
|
||||
};
|
||||
|
||||
const result = validateYarnProject(config);
|
||||
expect(result.valid).toBe(false);
|
||||
});
|
||||
|
||||
it('should reject non-array sourceFiles', () => {
|
||||
const config = {
|
||||
projectFileVersion: 4,
|
||||
sourceFiles: '**/*.yarn',
|
||||
baseLanguage: 'en',
|
||||
};
|
||||
|
||||
const result = validateYarnProject(config);
|
||||
expect(result.valid).toBe(false);
|
||||
});
|
||||
|
||||
it('should reject additional properties', () => {
|
||||
const config = {
|
||||
projectFileVersion: 4,
|
||||
sourceFiles: ['**/*.yarn'],
|
||||
baseLanguage: 'en',
|
||||
unknownField: 'value',
|
||||
};
|
||||
|
||||
const result = validateYarnProject(config);
|
||||
expect(result.valid).toBe(false);
|
||||
});
|
||||
|
||||
it('should accept definitions as string', () => {
|
||||
const config = {
|
||||
projectFileVersion: 4,
|
||||
sourceFiles: ['**/*.yarn'],
|
||||
baseLanguage: 'en',
|
||||
definitions: 'custom.ysls.json',
|
||||
};
|
||||
|
||||
const result = validateYarnProject(config);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
it('should accept definitions as array of strings', () => {
|
||||
const config = {
|
||||
projectFileVersion: 4,
|
||||
sourceFiles: ['**/*.yarn'],
|
||||
baseLanguage: 'en',
|
||||
definitions: ['custom1.ysls.json', 'custom2.ysls.json'],
|
||||
};
|
||||
|
||||
const result = validateYarnProject(config);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isYarnProject', () => {
|
||||
it('should return true for valid config', () => {
|
||||
const config = {
|
||||
projectFileVersion: 4,
|
||||
sourceFiles: ['**/*.yarn'],
|
||||
baseLanguage: 'en',
|
||||
};
|
||||
|
||||
expect(isYarnProject(config)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for invalid config', () => {
|
||||
const config = {
|
||||
sourceFiles: ['**/*.yarn'],
|
||||
baseLanguage: 'en',
|
||||
};
|
||||
|
||||
expect(isYarnProject(config)).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user