feat: impl
This commit is contained in:
parent
9942bd9a7f
commit
dd43bb1a1d
|
|
@ -0,0 +1,23 @@
|
||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Build output
|
||||||
|
dist/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
|
@ -0,0 +1,164 @@
|
||||||
|
# Yarn Spinner Loader
|
||||||
|
|
||||||
|
Load and compile Yarn Spinner project files (`.yarnproject`) for various build tools (esbuild, rollup, webpack, vite).
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- ✅ Parse and validate `.yarnproject` files against the official JSON schema
|
||||||
|
- ✅ Load and compile `.yarn` dialogue files using glob patterns
|
||||||
|
- ✅ Support for multiple build tools (esbuild, rollup, webpack, vite)
|
||||||
|
- ✅ TypeScript support with full type definitions
|
||||||
|
- ✅ Comprehensive test coverage with real fixtures
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install yarn-spinner-loader
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Core API
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
### With esbuild
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { build } from 'esbuild';
|
||||||
|
import { yarnSpinnerPlugin } from 'yarn-spinner-loader/esbuild';
|
||||||
|
|
||||||
|
build({
|
||||||
|
entryPoints: ['src/index.ts'],
|
||||||
|
plugins: [yarnSpinnerPlugin()],
|
||||||
|
bundle: true,
|
||||||
|
outfile: 'dist/bundle.js',
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### With Vite
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// vite.config.ts
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import { yarnSpinnerVite } from 'yarn-spinner-loader/vite';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [yarnSpinnerVite()],
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### With Rollup
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// rollup.config.js
|
||||||
|
import { yarnSpinnerRollup } from 'yarn-spinner-loader/rollup';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
input: 'src/main.js',
|
||||||
|
plugins: [yarnSpinnerRollup()],
|
||||||
|
output: {
|
||||||
|
file: 'dist/bundle.js',
|
||||||
|
format: 'es',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### With Webpack
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// webpack.config.js
|
||||||
|
module.exports = {
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.yarnproject$/,
|
||||||
|
use: 'yarn-spinner-loader/webpack',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Reference
|
||||||
|
|
||||||
|
### `loadYarnProject(path, options?)`
|
||||||
|
|
||||||
|
Load and compile a `.yarnproject` file and all its referenced `.yarn` files.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `path`: Path to the `.yarnproject` file
|
||||||
|
- `options.baseDir`: Base directory for resolving glob patterns (default: directory of `.yarnproject` file)
|
||||||
|
|
||||||
|
**Returns:** `Promise<LoadResult>`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface LoadResult {
|
||||||
|
project: YarnProject;
|
||||||
|
baseDir: string;
|
||||||
|
yarnFiles: Array<{
|
||||||
|
relativePath: string;
|
||||||
|
absolutePath: string;
|
||||||
|
document: YarnDocument;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `loadYarnProjectSync(path, options?)`
|
||||||
|
|
||||||
|
Synchronous version of `loadYarnProject`.
|
||||||
|
|
||||||
|
### `validateYarnProject(config)`
|
||||||
|
|
||||||
|
Validate a parsed `.yarnproject` object against the JSON schema.
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
```typescript
|
||||||
|
{ valid: true; data: YarnProject } | { valid: false; errors: ValidationError[] }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
yarn-spinner-loader/
|
||||||
|
├── src/
|
||||||
|
│ ├── loader/ # Core loader implementation
|
||||||
|
│ │ ├── index.ts # YarnProjectLoader
|
||||||
|
│ │ ├── validator.ts # JSON Schema validation
|
||||||
|
│ │ └── types.ts # TypeScript types
|
||||||
|
│ ├── plugins/ # Build tool plugins
|
||||||
|
│ │ ├── esbuild.ts
|
||||||
|
│ │ ├── rollup.ts
|
||||||
|
│ │ ├── vite.ts
|
||||||
|
│ │ └── webpack.ts
|
||||||
|
│ ├── yarn-spinner/ # Yarn Spinner parser (existing)
|
||||||
|
│ └── index.ts # Main entry point
|
||||||
|
├── tests/
|
||||||
|
│ ├── fixtures/ # Test fixtures
|
||||||
|
│ │ ├── simple/
|
||||||
|
│ │ ├── localised/
|
||||||
|
│ │ └── complex/
|
||||||
|
│ ├── validator.test.ts
|
||||||
|
│ └── loader.test.ts
|
||||||
|
└── package.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Run tests with vitest:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm test # Watch mode
|
||||||
|
npm run test:run # Run once
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,87 @@
|
||||||
|
{
|
||||||
|
"name": "yarn-spinner-loader",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Load and compile Yarn Spinner project files for various build tools",
|
||||||
|
"type": "module",
|
||||||
|
"main": "./dist/index.js",
|
||||||
|
"module": "./dist/index.js",
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"import": "./dist/index.js",
|
||||||
|
"require": "./dist/index.cjs"
|
||||||
|
},
|
||||||
|
"./esbuild": {
|
||||||
|
"types": "./dist/plugins/esbuild.d.ts",
|
||||||
|
"import": "./dist/plugins/esbuild.js",
|
||||||
|
"require": "./dist/plugins/esbuild.cjs"
|
||||||
|
},
|
||||||
|
"./rollup": {
|
||||||
|
"types": "./dist/plugins/rollup.d.ts",
|
||||||
|
"import": "./dist/plugins/rollup.js",
|
||||||
|
"require": "./dist/plugins/rollup.cjs"
|
||||||
|
},
|
||||||
|
"./vite": {
|
||||||
|
"types": "./dist/plugins/vite.d.ts",
|
||||||
|
"import": "./dist/plugins/vite.js",
|
||||||
|
"require": "./dist/plugins/vite.cjs"
|
||||||
|
},
|
||||||
|
"./webpack": {
|
||||||
|
"types": "./dist/plugins/webpack.d.ts",
|
||||||
|
"import": "./dist/plugins/webpack.js",
|
||||||
|
"require": "./dist/plugins/webpack.cjs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsup",
|
||||||
|
"dev": "tsup --watch",
|
||||||
|
"test": "vitest",
|
||||||
|
"test:run": "vitest run",
|
||||||
|
"typecheck": "tsc --noEmit"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"yarn-spinner",
|
||||||
|
"yarnproject",
|
||||||
|
"loader",
|
||||||
|
"esbuild",
|
||||||
|
"rollup",
|
||||||
|
"webpack",
|
||||||
|
"vite"
|
||||||
|
],
|
||||||
|
"author": "",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ajv": "^8.17.1",
|
||||||
|
"fast-glob": "^3.3.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.10.5",
|
||||||
|
"tsup": "^8.3.5",
|
||||||
|
"typescript": "^5.7.2",
|
||||||
|
"vitest": "^3.0.4"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"esbuild": ">=0.20.0",
|
||||||
|
"rollup": ">=4.0.0",
|
||||||
|
"vite": ">=5.0.0",
|
||||||
|
"webpack": ">=5.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"esbuild": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"rollup": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"vite": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"webpack": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
/**
|
||||||
|
* Yarn Spinner Loader - Main Entry Point
|
||||||
|
*
|
||||||
|
* Load and compile Yarn Spinner project files (.yarnproject)
|
||||||
|
* and their associated .yarn dialogue files.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export {
|
||||||
|
loadYarnProject,
|
||||||
|
loadYarnProjectSync,
|
||||||
|
type LoadOptions,
|
||||||
|
type LoadResult,
|
||||||
|
type LoadedYarnFile,
|
||||||
|
LoadError,
|
||||||
|
ValidationError as LoaderValidationError,
|
||||||
|
} from './loader/index';
|
||||||
|
|
||||||
|
export {
|
||||||
|
validateYarnProject,
|
||||||
|
isYarnProject,
|
||||||
|
type SchemaValidationError,
|
||||||
|
} from './loader/validator';
|
||||||
|
|
||||||
|
export type { YarnProject } from './loader/types';
|
||||||
|
|
||||||
|
// Re-export yarn-spinner parser
|
||||||
|
export { parseYarn } from './yarn-spinner/parse/parser';
|
||||||
|
export { compile } from './yarn-spinner/compile/compiler';
|
||||||
|
export { YarnRunner } from './yarn-spinner/runtime/runner';
|
||||||
|
|
@ -0,0 +1,199 @@
|
||||||
|
import { readFileSync, existsSync } from 'node:fs';
|
||||||
|
import { dirname, resolve, relative } from 'node:path';
|
||||||
|
import { glob } 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';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options for loading a Yarn project
|
||||||
|
*/
|
||||||
|
export interface LoadOptions {
|
||||||
|
/** Base directory for resolving glob patterns (default: directory of .yarnproject file) */
|
||||||
|
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
|
||||||
|
*/
|
||||||
|
export interface LoadResult {
|
||||||
|
/** Parsed .yarnproject configuration */
|
||||||
|
project: YarnProject;
|
||||||
|
/** Directory containing the .yarnproject file */
|
||||||
|
baseDir: string;
|
||||||
|
/** All loaded and parsed .yarn files */
|
||||||
|
yarnFiles: LoadedYarnFile[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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> {
|
||||||
|
// Resolve and read .yarnproject file
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate against schema
|
||||||
|
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);
|
||||||
|
|
||||||
|
// Find all .yarn files using glob patterns
|
||||||
|
const sourcePatterns = project.sourceFiles;
|
||||||
|
const ignorePatterns = project.excludeFiles || [];
|
||||||
|
|
||||||
|
const yarnFilePaths = await glob(sourcePatterns, {
|
||||||
|
cwd: baseDir,
|
||||||
|
ignore: ignorePatterns,
|
||||||
|
absolute: true,
|
||||||
|
onlyFiles: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load and parse each .yarn file
|
||||||
|
const yarnFiles: LoadedYarnFile[] = yarnFilePaths.map(absolutePath => {
|
||||||
|
const relativePath = relative(baseDir, absolutePath);
|
||||||
|
const content = readFileSync(absolutePath, 'utf-8');
|
||||||
|
const document = parseYarn(content);
|
||||||
|
|
||||||
|
return {
|
||||||
|
relativePath,
|
||||||
|
absolutePath,
|
||||||
|
document,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
project,
|
||||||
|
baseDir,
|
||||||
|
yarnFiles,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Synchronous version of loadYarnProject
|
||||||
|
*/
|
||||||
|
export function loadYarnProjectSync(
|
||||||
|
projectFilePath: string,
|
||||||
|
options: LoadOptions = {},
|
||||||
|
): LoadResult {
|
||||||
|
// Resolve and read .yarnproject file
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate against schema
|
||||||
|
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);
|
||||||
|
|
||||||
|
// Find all .yarn files using glob patterns
|
||||||
|
const sourcePatterns = project.sourceFiles;
|
||||||
|
const ignorePatterns = project.excludeFiles || [];
|
||||||
|
|
||||||
|
const yarnFilePaths = glob.sync(sourcePatterns, {
|
||||||
|
cwd: baseDir,
|
||||||
|
ignore: ignorePatterns,
|
||||||
|
absolute: true,
|
||||||
|
onlyFiles: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load and parse each .yarn file
|
||||||
|
const yarnFiles: LoadedYarnFile[] = yarnFilePaths.map(absolutePath => {
|
||||||
|
const relativePath = relative(baseDir, absolutePath);
|
||||||
|
const content = readFileSync(absolutePath, 'utf-8');
|
||||||
|
const document = parseYarn(content);
|
||||||
|
|
||||||
|
return {
|
||||||
|
relativePath,
|
||||||
|
absolutePath,
|
||||||
|
document,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
project,
|
||||||
|
baseDir,
|
||||||
|
yarnFiles,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
export interface YarnProject {
|
||||||
|
projectFileVersion: number;
|
||||||
|
projectName?: string;
|
||||||
|
authorName?: string[];
|
||||||
|
sourceFiles: string[];
|
||||||
|
excludeFiles?: string[];
|
||||||
|
baseLanguage: string;
|
||||||
|
localisation?: {
|
||||||
|
[languageCode: string]: {
|
||||||
|
strings?: string;
|
||||||
|
assets?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
definitions?: string | string[];
|
||||||
|
compilerOptions?: {
|
||||||
|
requireVariableDeclarations?: boolean;
|
||||||
|
allowPreviewFeatures?: boolean;
|
||||||
|
};
|
||||||
|
editorOptions?: {
|
||||||
|
yarnScriptEditor?: Record<string, unknown>;
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,124 @@
|
||||||
|
import Ajv from 'ajv';
|
||||||
|
import type { YarnProject } from './types.js';
|
||||||
|
|
||||||
|
export interface SchemaValidationError {
|
||||||
|
path: string;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON Schema for .yarnproject files
|
||||||
|
* Based on https://schemas.yarnspinner.dev/yarnproject.schema.json
|
||||||
|
*/
|
||||||
|
const YARNPROJECT_SCHEMA = {
|
||||||
|
$schema: 'http://json-schema.org/draft-07/schema#',
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
projectFileVersion: {
|
||||||
|
type: 'integer',
|
||||||
|
minimum: 2,
|
||||||
|
default: 4,
|
||||||
|
},
|
||||||
|
projectName: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
authorName: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sourceFiles: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
default: ['**/*.yarn'],
|
||||||
|
},
|
||||||
|
excludeFiles: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
baseLanguage: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
localisation: {
|
||||||
|
type: 'object',
|
||||||
|
additionalProperties: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
strings: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
assets: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
definitions: {
|
||||||
|
oneOf: [
|
||||||
|
{ type: 'string' },
|
||||||
|
{
|
||||||
|
type: 'array',
|
||||||
|
items: { type: 'string' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
compilerOptions: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
requireVariableDeclarations: {
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
allowPreviewFeatures: {
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
additionalProperties: true,
|
||||||
|
},
|
||||||
|
editorOptions: {
|
||||||
|
type: 'object',
|
||||||
|
additionalProperties: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['projectFileVersion', 'sourceFiles', 'baseLanguage'],
|
||||||
|
additionalProperties: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const ajv = new Ajv({ allErrors: true });
|
||||||
|
const validate = ajv.compile(YARNPROJECT_SCHEMA);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate a parsed .yarnproject object against the schema
|
||||||
|
*/
|
||||||
|
export function validateYarnProject(config: unknown): { valid: true; data: YarnProject } | { valid: false; errors: SchemaValidationError[] } {
|
||||||
|
const valid = validate(config);
|
||||||
|
|
||||||
|
if (valid) {
|
||||||
|
// Check required fields before casting
|
||||||
|
const obj = config as Record<string, unknown>;
|
||||||
|
if (obj.projectFileVersion && obj.sourceFiles && obj.baseLanguage) {
|
||||||
|
return { valid: true, data: obj as unknown as YarnProject };
|
||||||
|
}
|
||||||
|
return { valid: true, data: obj as unknown as YarnProject };
|
||||||
|
}
|
||||||
|
|
||||||
|
const errors: SchemaValidationError[] = (validate.errors || []).map((err: any) => ({
|
||||||
|
path: err.instancePath || '/',
|
||||||
|
message: err.message || 'Unknown error',
|
||||||
|
}));
|
||||||
|
|
||||||
|
return { valid: false, errors };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard to check if config is a valid YarnProject
|
||||||
|
*/
|
||||||
|
export function isYarnProject(config: unknown): config is YarnProject {
|
||||||
|
return validate(config) === true;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
import type { Plugin } from 'esbuild';
|
||||||
|
import { loadYarnProject, type LoadResult } from '../loader/index.js';
|
||||||
|
|
||||||
|
export interface YarnSpinnerEsbuildOptions {
|
||||||
|
/** Additional options passed to loadYarnProject */
|
||||||
|
loadOptions?: Parameters<typeof loadYarnProject>[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Esbuild plugin for loading Yarn Spinner project files
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* import { yarnSpinnerPlugin } from 'yarn-spinner-loader/esbuild';
|
||||||
|
*
|
||||||
|
* esbuild.build({
|
||||||
|
* plugins: [yarnSpinnerPlugin()],
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function yarnSpinnerPlugin(options: YarnSpinnerEsbuildOptions = {}): Plugin {
|
||||||
|
return {
|
||||||
|
name: 'yarn-spinner-loader',
|
||||||
|
setup(build) {
|
||||||
|
// Intercept .yarnproject files
|
||||||
|
build.onLoad({ filter: /\.yarnproject$/ }, async (args) => {
|
||||||
|
try {
|
||||||
|
const result: LoadResult = await loadYarnProject(args.path, options.loadOptions);
|
||||||
|
|
||||||
|
return {
|
||||||
|
contents: `export default ${JSON.stringify(result, null, 2)};`,
|
||||||
|
loader: 'js',
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
errors: [{ text: error instanceof Error ? error.message : String(error) }],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
import type { Plugin } from 'rollup';
|
||||||
|
import { loadYarnProject, type LoadResult } from '../loader/index.js';
|
||||||
|
|
||||||
|
export interface YarnSpinnerRollupOptions {
|
||||||
|
/** Additional options passed to loadYarnProject */
|
||||||
|
loadOptions?: Parameters<typeof loadYarnProject>[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rollup plugin for loading Yarn Spinner project files
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* import { yarnSpinnerRollup } from 'yarn-spinner-loader/rollup';
|
||||||
|
*
|
||||||
|
* rollup({
|
||||||
|
* plugins: [yarnSpinnerRollup()],
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function yarnSpinnerRollup(options: YarnSpinnerRollupOptions = {}): Plugin {
|
||||||
|
return {
|
||||||
|
name: 'yarn-spinner-loader',
|
||||||
|
async load(id) {
|
||||||
|
if (id.endsWith('.yarnproject')) {
|
||||||
|
try {
|
||||||
|
const result: LoadResult = await loadYarnProject(id, options.loadOptions);
|
||||||
|
return `export default ${JSON.stringify(result, null, 2)};`;
|
||||||
|
} catch (error) {
|
||||||
|
this.error(error instanceof Error ? error.message : String(error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
import type { Plugin } from 'vite';
|
||||||
|
import { loadYarnProject, type LoadResult } from '../loader/index.js';
|
||||||
|
|
||||||
|
export interface YarnSpinnerViteOptions {
|
||||||
|
/** Additional options passed to loadYarnProject */
|
||||||
|
loadOptions?: Parameters<typeof loadYarnProject>[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vite plugin for loading Yarn Spinner project files
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* import { yarnSpinnerVite } from 'yarn-spinner-loader/vite';
|
||||||
|
*
|
||||||
|
* export default defineConfig({
|
||||||
|
* plugins: [yarnSpinnerVite()],
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function yarnSpinnerVite(options: YarnSpinnerViteOptions = {}): Plugin {
|
||||||
|
return {
|
||||||
|
name: 'yarn-spinner-loader',
|
||||||
|
async load(id) {
|
||||||
|
if (id.endsWith('.yarnproject')) {
|
||||||
|
try {
|
||||||
|
const result: LoadResult = await loadYarnProject(id, options.loadOptions);
|
||||||
|
return `export default ${JSON.stringify(result, null, 2)};`;
|
||||||
|
} catch (error) {
|
||||||
|
this.error(error instanceof Error ? error.message : String(error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
transform(_src, id) {
|
||||||
|
if (id.endsWith('.yarnproject')) {
|
||||||
|
return {
|
||||||
|
code: `export default ${JSON.stringify({}, null, 2)};`,
|
||||||
|
map: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
import { loadYarnProjectSync, type LoadResult } from '../loader/index.js';
|
||||||
|
|
||||||
|
export interface YarnSpinnerWebpackOptions {
|
||||||
|
/** Additional options passed to loadYarnProject */
|
||||||
|
loadOptions?: Parameters<typeof loadYarnProjectSync>[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Webpack loader for loading Yarn Spinner project files
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* // webpack.config.js
|
||||||
|
* module.exports = {
|
||||||
|
* module: {
|
||||||
|
* rules: [
|
||||||
|
* {
|
||||||
|
* test: /\.yarnproject$/,
|
||||||
|
* use: 'yarn-spinner-loader/webpack',
|
||||||
|
* },
|
||||||
|
* ],
|
||||||
|
* },
|
||||||
|
* };
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function YarnSpinnerWebpackLoader(this: any, source: string) {
|
||||||
|
const options: YarnSpinnerWebpackOptions = this.getOptions() || {};
|
||||||
|
const resourcePath = this.resourcePath;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result: LoadResult = loadYarnProjectSync(resourcePath, options.loadOptions);
|
||||||
|
const code = `export default ${JSON.stringify(result, null, 2)};`;
|
||||||
|
return code;
|
||||||
|
} catch (error) {
|
||||||
|
this.emitError(error instanceof Error ? error : new Error(String(error)));
|
||||||
|
return source;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default YarnSpinnerWebpackLoader;
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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.
|
||||||
|
===
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
title: ScriptNode
|
||||||
|
---
|
||||||
|
<<set $variable = 1>>
|
||||||
|
<<set $another = true>>
|
||||||
|
===
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
title: Welcome
|
||||||
|
---
|
||||||
|
Welcome to the localised project!
|
||||||
|
This text should be translated.
|
||||||
|
===
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"projectFileVersion": 4,
|
||||||
|
"projectName": "Simple Test Project",
|
||||||
|
"sourceFiles": ["**/*.yarn"],
|
||||||
|
"baseLanguage": "en"
|
||||||
|
}
|
||||||
|
|
@ -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!
|
||||||
|
===
|
||||||
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"lib": ["ES2022"],
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"sourceMap": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist", "**/*.test.ts"]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
import { defineConfig } from 'tsup';
|
||||||
|
|
||||||
|
export default defineConfig([
|
||||||
|
{
|
||||||
|
entry: ['src/index.ts'],
|
||||||
|
format: ['esm', 'cjs'],
|
||||||
|
dts: true,
|
||||||
|
sourcemap: true,
|
||||||
|
clean: true,
|
||||||
|
outDir: 'dist',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
entry: ['src/plugins/esbuild.ts'],
|
||||||
|
format: ['esm', 'cjs'],
|
||||||
|
dts: true,
|
||||||
|
sourcemap: true,
|
||||||
|
outDir: 'dist/plugins',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
entry: ['src/plugins/rollup.ts'],
|
||||||
|
format: ['esm', 'cjs'],
|
||||||
|
dts: true,
|
||||||
|
sourcemap: true,
|
||||||
|
outDir: 'dist/plugins',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
entry: ['src/plugins/vite.ts'],
|
||||||
|
format: ['esm', 'cjs'],
|
||||||
|
dts: true,
|
||||||
|
sourcemap: true,
|
||||||
|
outDir: 'dist/plugins',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
entry: ['src/plugins/webpack.ts'],
|
||||||
|
format: ['esm', 'cjs'],
|
||||||
|
dts: true,
|
||||||
|
sourcemap: true,
|
||||||
|
outDir: 'dist/plugins',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { defineConfig } from 'vitest/config';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
globals: false,
|
||||||
|
environment: 'node',
|
||||||
|
include: ['tests/**/*.test.ts'],
|
||||||
|
exclude: ['node_modules', 'dist'],
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
// Allow .js imports to resolve to .ts files
|
||||||
|
},
|
||||||
|
},
|
||||||
|
esbuild: {
|
||||||
|
target: 'es2022',
|
||||||
|
},
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue