Compare commits
8
Commits
38408f774e
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
99c381d6d2 | ||
|
|
9f3d59b0bc | ||
|
|
0fd35bfdab | ||
|
|
73d49cb954 | ||
|
|
f5989a162b | ||
|
|
0ecccb6fee | ||
|
|
5bf3d6b769 | ||
|
|
6b489ede0e |
@@ -0,0 +1,40 @@
|
||||
# AGENTS.md - Yarn Spinner Loader
|
||||
|
||||
## Commands
|
||||
- `npm run build` — Build all entry points (tsup, ESM + CJS)
|
||||
- `npm run dev` — Watch mode
|
||||
- `npm run test` — Vitest watch mode
|
||||
- `npm run test:run` — Run tests once
|
||||
- `npm run typecheck` — `tsc --noEmit`
|
||||
|
||||
## Architecture
|
||||
- **`src/index.ts`** — Main entry point. Full browser package: parse + compile + run + all types. No Node.js deps.
|
||||
- **`src/core.ts`** — Mirror of index.ts. Not in package.json exports.
|
||||
- **`src/runner.ts`** — Minimal runtime export: `YarnRunner` + runtime/IR types only. No parser, no compiler.
|
||||
- **`src/loader/`** — Filesystem loader: parses `.yarnproject` files, validates against JSON schema (ajv), resolves globs (fast-glob), compiles `.yarn` files.
|
||||
- **`src/plugins/`** — Build tool plugins: esbuild, rollup, vite, webpack. Each is a separate tsup entry point outputting to `dist/plugins/`.
|
||||
- **`src/yarn-spinner/`** — Embedded Yarn Spinner implementation: `parse/`, `compile/`, `runtime/`, `markup/`, `model/`.
|
||||
- **`src/runner/`** — Empty directory. Ignore.
|
||||
|
||||
## Package exports
|
||||
- `yarn-spinner-loader` → `dist/index.js` (full browser package)
|
||||
- `yarn-spinner-loader/runner` → `dist/runner.js` (runtime only)
|
||||
- `yarn-spinner-loader/esbuild` → `dist/plugins/esbuild.js`
|
||||
- `yarn-spinner-loader/rollup` → `dist/plugins/rollup.js`
|
||||
- `yarn-spinner-loader/vite` → `dist/plugins/vite.js`
|
||||
- `yarn-spinner-loader/webpack` → `dist/plugins/webpack.js`
|
||||
|
||||
## Key conventions
|
||||
- `src/index.ts` and `src/core.ts` must NOT import from `src/loader/` or any Node.js-specific modules.
|
||||
- `src/loader/` is the only code that uses `fs`, `path`, `fast-glob`.
|
||||
- TypeScript: strict mode, ES2022, `moduleResolution: "bundler"`.
|
||||
- Tests live in `tests/`, not `src/`. Pattern: `tests/**/*.test.ts`.
|
||||
|
||||
## Build notes
|
||||
- tsup config has 6 entry points. Main + runner output to `dist/`, plugins output to `dist/plugins/`.
|
||||
- `npm run build` cleans `dist/` automatically.
|
||||
- `core.ts` is NOT exported in package.json — it exists for internal consistency only.
|
||||
- This package is intended as a `devDependency` alongside bundlers. Dependencies (`ajv`, `fast-glob`) are needed at build time.
|
||||
|
||||
## Security
|
||||
- `.npmrc` contains an npm auth token. Never commit or expose this.
|
||||
@@ -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;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "yarn-spinner-loader",
|
||||
"version": "0.1.0",
|
||||
"version": "0.2.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "yarn-spinner-loader",
|
||||
"version": "0.1.0",
|
||||
"version": "0.2.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ajv": "^8.17.1",
|
||||
|
||||
+5
-5
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "yarn-spinner-loader",
|
||||
"version": "0.1.0",
|
||||
"version": "0.3.0",
|
||||
"description": "Load and compile Yarn Spinner project files for various build tools",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
@@ -12,10 +12,10 @@
|
||||
"import": "./dist/index.js",
|
||||
"require": "./dist/index.cjs"
|
||||
},
|
||||
"./node": {
|
||||
"types": "./dist/node.d.ts",
|
||||
"import": "./dist/node.js",
|
||||
"require": "./dist/node.cjs"
|
||||
"./runner": {
|
||||
"types": "./dist/runner.d.ts",
|
||||
"import": "./dist/runner.js",
|
||||
"require": "./dist/runner.cjs"
|
||||
},
|
||||
"./esbuild": {
|
||||
"types": "./dist/plugins/esbuild.d.ts",
|
||||
|
||||
+54
-11
@@ -5,21 +5,64 @@
|
||||
* This module does NOT depend on Node.js modules.
|
||||
*/
|
||||
|
||||
// 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';
|
||||
// Parser
|
||||
export { parseYarn, ParseError } from './yarn-spinner/parse/parser';
|
||||
|
||||
// Re-export types
|
||||
// Compiler
|
||||
export { compile, type CompileOptions } from './yarn-spinner/compile/compiler';
|
||||
|
||||
// Runner
|
||||
export { YarnRunner, type RunnerOptions } from './yarn-spinner/runtime/runner';
|
||||
|
||||
// AST types
|
||||
export type {
|
||||
Position,
|
||||
NodeHeaderMap,
|
||||
YarnDocument,
|
||||
EnumDefinition,
|
||||
YarnNode,
|
||||
Statement,
|
||||
Line,
|
||||
Command,
|
||||
Jump,
|
||||
Detour,
|
||||
Return,
|
||||
OptionGroup,
|
||||
Option,
|
||||
IfBlock,
|
||||
OnceBlock,
|
||||
EnumBlock,
|
||||
} from './yarn-spinner/model/ast';
|
||||
|
||||
// IR types
|
||||
export type {
|
||||
IRProgram,
|
||||
IRNode,
|
||||
IRNodeGroup,
|
||||
IRInstruction,
|
||||
} from './yarn-spinner/compile/ir';
|
||||
|
||||
// Runtime types
|
||||
export type {
|
||||
LoadOptions,
|
||||
LoadResult,
|
||||
LoadedYarnFile,
|
||||
YarnProject,
|
||||
SchemaValidationError,
|
||||
RunnerOptions,
|
||||
TextResult,
|
||||
OptionsResult,
|
||||
CommandResult,
|
||||
RuntimeResult,
|
||||
} from './yarn-spinner/runtime/results';
|
||||
|
||||
// Markup types
|
||||
export type {
|
||||
MarkupValue,
|
||||
MarkupWrapperType,
|
||||
MarkupWrapper,
|
||||
MarkupSegment,
|
||||
MarkupParseResult,
|
||||
} from './yarn-spinner/markup/types';
|
||||
|
||||
// Loader types
|
||||
export type {
|
||||
LoadOptions,
|
||||
LoadResult,
|
||||
YarnProject,
|
||||
SchemaValidationError,
|
||||
} from './types';
|
||||
|
||||
+56
-13
@@ -1,26 +1,69 @@
|
||||
/**
|
||||
* Yarn Spinner Loader - Main Entry Point
|
||||
*
|
||||
* Pure API for working with Yarn Spinner documents.
|
||||
* Full browser-compatible package: parse, compile, and run Yarn Spinner documents.
|
||||
* This module does NOT depend on Node.js modules.
|
||||
* For filesystem operations, use the 'yarn-spinner-loader/node' entry point.
|
||||
* For filesystem loading, use the 'yarn-spinner-loader/node' entry point.
|
||||
*/
|
||||
|
||||
// Re-export yarn-spinner parser and runtime
|
||||
export { parseYarn } from './yarn-spinner/parse/parser';
|
||||
export { compile } from './yarn-spinner/compile/compiler';
|
||||
export { YarnRunner } from './yarn-spinner/runtime/runner';
|
||||
// Parser
|
||||
export { parseYarn, ParseError } from './yarn-spinner/parse/parser';
|
||||
|
||||
// Re-export types
|
||||
// Compiler
|
||||
export { compile, type CompileOptions } from './yarn-spinner/compile/compiler';
|
||||
|
||||
// Runner
|
||||
export { YarnRunner, type RunnerOptions } from './yarn-spinner/runtime/runner';
|
||||
|
||||
// AST types
|
||||
export type {
|
||||
Position,
|
||||
NodeHeaderMap,
|
||||
YarnDocument,
|
||||
EnumDefinition,
|
||||
YarnNode,
|
||||
Statement,
|
||||
Line,
|
||||
Command,
|
||||
Jump,
|
||||
Detour,
|
||||
Return,
|
||||
OptionGroup,
|
||||
Option,
|
||||
IfBlock,
|
||||
OnceBlock,
|
||||
EnumBlock,
|
||||
} from './yarn-spinner/model/ast';
|
||||
|
||||
// IR types
|
||||
export type {
|
||||
IRProgram,
|
||||
IRNode,
|
||||
IRNodeGroup,
|
||||
IRInstruction,
|
||||
} from './yarn-spinner/compile/ir';
|
||||
|
||||
// Runtime types
|
||||
export type {
|
||||
LoadOptions,
|
||||
LoadResult,
|
||||
LoadedYarnFile,
|
||||
YarnProject,
|
||||
SchemaValidationError,
|
||||
RunnerOptions,
|
||||
TextResult,
|
||||
OptionsResult,
|
||||
CommandResult,
|
||||
RuntimeResult,
|
||||
} from './yarn-spinner/runtime/results';
|
||||
|
||||
// Markup types
|
||||
export type {
|
||||
MarkupValue,
|
||||
MarkupWrapperType,
|
||||
MarkupWrapper,
|
||||
MarkupSegment,
|
||||
MarkupParseResult,
|
||||
} from './yarn-spinner/markup/types';
|
||||
|
||||
// Loader types
|
||||
export type {
|
||||
LoadOptions,
|
||||
LoadResult,
|
||||
YarnProject,
|
||||
SchemaValidationError,
|
||||
} from './types';
|
||||
|
||||
+24
-45
@@ -1,10 +1,11 @@
|
||||
import { readFileSync, existsSync } from 'node:fs';
|
||||
import { dirname, resolve, relative } from 'node:path';
|
||||
import { glob } from 'fast-glob';
|
||||
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<LoadResult> {
|
||||
// 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,34 +87,31 @@ 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 || [];
|
||||
|
||||
const yarnFilePaths = await glob(sourcePatterns, {
|
||||
const yarnFilePaths = await fg.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 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,33 +149,30 @@ 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 || [];
|
||||
|
||||
const yarnFilePaths = glob.sync(sourcePatterns, {
|
||||
const yarnFilePaths = fg.globSync(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 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,
|
||||
};
|
||||
}
|
||||
|
||||
-24
@@ -1,24 +0,0 @@
|
||||
/**
|
||||
* Yarn Spinner Loader - Node.js API
|
||||
*
|
||||
* Functions that depend on Node.js modules (fs, path, fast-glob).
|
||||
* Use this entry point when you need to load .yarnproject files from the filesystem.
|
||||
*/
|
||||
|
||||
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';
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Yarn Spinner Loader - Runner API
|
||||
*
|
||||
* Minimal runtime-only export for executing pre-compiled IRPrograms.
|
||||
* No parser, no compiler — just the runner and its types.
|
||||
*/
|
||||
|
||||
export { YarnRunner, type RunnerOptions } from './yarn-spinner/runtime/runner';
|
||||
|
||||
export type {
|
||||
TextResult,
|
||||
OptionsResult,
|
||||
CommandResult,
|
||||
RuntimeResult,
|
||||
} from './yarn-spinner/runtime/results';
|
||||
|
||||
export type {
|
||||
IRProgram,
|
||||
IRNode,
|
||||
IRNodeGroup,
|
||||
IRInstruction,
|
||||
} from './yarn-spinner/compile/ir';
|
||||
|
||||
export type {
|
||||
MarkupParseResult,
|
||||
MarkupSegment,
|
||||
MarkupWrapper,
|
||||
MarkupWrapperType,
|
||||
MarkupValue,
|
||||
} from './yarn-spinner/markup/types';
|
||||
+4
-16
@@ -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;
|
||||
}
|
||||
|
||||
+10
-41
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { yarnSpinnerPlugin } from '../../src/plugins/esbuild';
|
||||
import { resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
||||
|
||||
describe('yarnSpinnerPlugin (esbuild)', () => {
|
||||
// Fixtures are in tests/fixtures/, not tests/plugins/fixtures/
|
||||
const projectPath = resolve(__dirname, '../fixtures/simple/.yarnproject');
|
||||
|
||||
it('should return an esbuild plugin with correct name', () => {
|
||||
const plugin = yarnSpinnerPlugin();
|
||||
expect(plugin.name).toBe('yarn-spinner-loader');
|
||||
});
|
||||
|
||||
it('should have a setup function', () => {
|
||||
const plugin = yarnSpinnerPlugin();
|
||||
expect(plugin.setup).toBeDefined();
|
||||
expect(typeof plugin.setup).toBe('function');
|
||||
});
|
||||
|
||||
it('should register onLoad handler for .yarnproject files', () => {
|
||||
const plugin = yarnSpinnerPlugin();
|
||||
|
||||
const mockBuild = {
|
||||
onLoad: vi.fn((_options, callback) => {
|
||||
// Store the callback for testing
|
||||
(mockBuild as any)._onLoadCallback = callback;
|
||||
}),
|
||||
};
|
||||
|
||||
plugin.setup(mockBuild as any);
|
||||
|
||||
expect(mockBuild.onLoad).toHaveBeenCalledWith(
|
||||
{ filter: /\.yarnproject$/ },
|
||||
expect.any(Function),
|
||||
);
|
||||
});
|
||||
|
||||
it('should load .yarnproject files and return correct content', async () => {
|
||||
const plugin = yarnSpinnerPlugin();
|
||||
|
||||
let onLoadCallback: Function | undefined;
|
||||
const mockBuild = {
|
||||
onLoad: vi.fn((_options, callback) => {
|
||||
onLoadCallback = callback;
|
||||
}),
|
||||
};
|
||||
|
||||
plugin.setup(mockBuild as any);
|
||||
|
||||
// Ensure callback was registered
|
||||
expect(onLoadCallback).toBeDefined();
|
||||
|
||||
// Simulate esbuild calling the callback
|
||||
const args = { path: projectPath };
|
||||
const result = await (onLoadCallback as any)(args);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.contents).toContain('export default');
|
||||
expect(result.contents).toContain('project');
|
||||
expect(result.contents).toContain('program');
|
||||
expect(result.loader).toBe('js');
|
||||
});
|
||||
|
||||
it('should handle errors by returning error object', async () => {
|
||||
const plugin = yarnSpinnerPlugin();
|
||||
|
||||
let onLoadCallback: Function | undefined;
|
||||
const mockBuild = {
|
||||
onLoad: vi.fn((_options, callback) => {
|
||||
onLoadCallback = callback;
|
||||
}),
|
||||
};
|
||||
|
||||
plugin.setup(mockBuild as any);
|
||||
|
||||
// Simulate esbuild calling the callback with non-existent file
|
||||
const args = { path: 'nonexistent/.yarnproject' };
|
||||
const result = await (onLoadCallback as any)(args);
|
||||
|
||||
expect(result.errors).toBeDefined();
|
||||
expect(result.errors.length).toBeGreaterThan(0);
|
||||
expect(result.errors[0].text).toBeDefined();
|
||||
expect(typeof result.errors[0].text).toBe('string');
|
||||
});
|
||||
|
||||
it('should accept loadOptions and pass them to loadYarnProject', async () => {
|
||||
const plugin = yarnSpinnerPlugin({
|
||||
loadOptions: {
|
||||
baseDir: resolve(__dirname, 'fixtures/simple'),
|
||||
},
|
||||
});
|
||||
|
||||
let onLoadCallback: Function | undefined;
|
||||
const mockBuild = {
|
||||
onLoad: vi.fn((_options, callback) => {
|
||||
onLoadCallback = callback;
|
||||
}),
|
||||
};
|
||||
|
||||
plugin.setup(mockBuild as any);
|
||||
|
||||
const args = { path: projectPath };
|
||||
const result = await (onLoadCallback as any)(args);
|
||||
|
||||
expect(result.contents).toContain('export default');
|
||||
expect(result.loader).toBe('js');
|
||||
});
|
||||
|
||||
it('should correctly serialize the result as JSON with proper formatting', async () => {
|
||||
const plugin = yarnSpinnerPlugin();
|
||||
|
||||
let onLoadCallback: Function | undefined;
|
||||
const mockBuild = {
|
||||
onLoad: vi.fn((_options, callback) => {
|
||||
onLoadCallback = callback;
|
||||
}),
|
||||
};
|
||||
|
||||
plugin.setup(mockBuild as any);
|
||||
|
||||
const args = { path: projectPath };
|
||||
const result = await (onLoadCallback as any)(args);
|
||||
|
||||
// The contents should be properly formatted JavaScript code
|
||||
expect(result.contents).toMatch(/export default {[\s\S]*};/);
|
||||
expect(result.loader).toBe('js');
|
||||
});
|
||||
|
||||
it('should handle non-Error objects in error handling', async () => {
|
||||
const plugin = yarnSpinnerPlugin();
|
||||
|
||||
let onLoadCallback: Function | undefined;
|
||||
const mockBuild = {
|
||||
onLoad: vi.fn((_options, callback) => {
|
||||
onLoadCallback = callback;
|
||||
}),
|
||||
};
|
||||
|
||||
plugin.setup(mockBuild as any);
|
||||
|
||||
const args = { path: 'nonexistent/.yarnproject' };
|
||||
const result = await (onLoadCallback as any)(args);
|
||||
|
||||
expect(result.errors[0].text).toBeDefined();
|
||||
expect(typeof result.errors[0].text).toBe('string');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,104 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { yarnSpinnerRollup } from '../../src/plugins/rollup';
|
||||
import { resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
||||
|
||||
describe('yarnSpinnerRollup', () => {
|
||||
// Fixtures are in tests/fixtures/, not tests/plugins/fixtures/
|
||||
const projectPath = resolve(__dirname, '../fixtures/simple/.yarnproject');
|
||||
|
||||
it('should return a Rollup plugin with correct name', () => {
|
||||
const plugin = yarnSpinnerRollup();
|
||||
expect(plugin.name).toBe('yarn-spinner-loader');
|
||||
});
|
||||
|
||||
it('should have a load function', () => {
|
||||
const plugin = yarnSpinnerRollup();
|
||||
expect(plugin.load).toBeDefined();
|
||||
expect(typeof plugin.load).toBe('function');
|
||||
});
|
||||
|
||||
it('should load .yarnproject files and return exported code', async () => {
|
||||
const plugin = yarnSpinnerRollup();
|
||||
const loadFn = plugin.load as Function;
|
||||
|
||||
const mockContext = {
|
||||
error: vi.fn(),
|
||||
};
|
||||
|
||||
const result = await loadFn.call(mockContext, projectPath);
|
||||
|
||||
// If error was called, the file wasn't found or was invalid
|
||||
if (mockContext.error.mock.calls.length > 0) {
|
||||
throw new Error(mockContext.error.mock.calls[0][0]);
|
||||
}
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(typeof result).toBe('string');
|
||||
expect(result).toContain('export default');
|
||||
expect(result).toContain('project');
|
||||
expect(result).toContain('program');
|
||||
});
|
||||
|
||||
it('should return null for non-.yarnproject files', async () => {
|
||||
const plugin = yarnSpinnerRollup();
|
||||
const loadFn = plugin.load as Function;
|
||||
|
||||
const result = await loadFn.call({}, 'some-file.ts');
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle errors by calling this.error', async () => {
|
||||
const plugin = yarnSpinnerRollup();
|
||||
const loadFn = plugin.load as Function;
|
||||
|
||||
const mockThis = {
|
||||
error: vi.fn(),
|
||||
};
|
||||
|
||||
await loadFn.call(mockThis, 'nonexistent/.yarnproject');
|
||||
|
||||
expect(mockThis.error).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should accept loadOptions and pass them to loadYarnProject', async () => {
|
||||
const plugin = yarnSpinnerRollup({
|
||||
loadOptions: {
|
||||
baseDir: resolve(__dirname, '../fixtures/simple'),
|
||||
},
|
||||
});
|
||||
const loadFn = plugin.load as Function;
|
||||
|
||||
const mockContext = {
|
||||
error: vi.fn(),
|
||||
};
|
||||
|
||||
const result = await loadFn.call(mockContext, projectPath);
|
||||
|
||||
expect(mockContext.error).not.toHaveBeenCalled();
|
||||
expect(result).toBeDefined();
|
||||
expect(typeof result).toBe('string');
|
||||
expect(result).toContain('export default');
|
||||
});
|
||||
|
||||
it('should correctly serialize the result as JSON', async () => {
|
||||
const plugin = yarnSpinnerRollup();
|
||||
const loadFn = plugin.load as Function;
|
||||
|
||||
const mockContext = {
|
||||
error: vi.fn(),
|
||||
};
|
||||
|
||||
const result = await loadFn.call(mockContext, projectPath);
|
||||
|
||||
if (mockContext.error.mock.calls.length > 0) {
|
||||
throw new Error(mockContext.error.mock.calls[0][0]);
|
||||
}
|
||||
|
||||
// The result should be valid JavaScript code
|
||||
expect(result).toMatch(/export default {[\s\S]*};/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,113 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { yarnSpinnerVite } from '../../src/plugins/vite';
|
||||
import { resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
||||
|
||||
describe('yarnSpinnerVite', () => {
|
||||
// Fixtures are in tests/fixtures/, not tests/plugins/fixtures/
|
||||
const projectPath = resolve(__dirname, '../fixtures/simple/.yarnproject');
|
||||
|
||||
it('should return a Vite plugin with correct name', () => {
|
||||
const plugin = yarnSpinnerVite();
|
||||
expect(plugin.name).toBe('yarn-spinner-loader');
|
||||
});
|
||||
|
||||
it('should have a load function', () => {
|
||||
const plugin = yarnSpinnerVite();
|
||||
expect(plugin.load).toBeDefined();
|
||||
expect(typeof plugin.load).toBe('function');
|
||||
});
|
||||
|
||||
it('should load .yarnproject files and return exported code', async () => {
|
||||
const plugin = yarnSpinnerVite();
|
||||
const loadFn = plugin.load as Function;
|
||||
|
||||
const mockContext = {
|
||||
error: vi.fn(),
|
||||
};
|
||||
|
||||
const result = await loadFn.call(mockContext, projectPath);
|
||||
|
||||
// If error was called, the file wasn't found or was invalid
|
||||
if (mockContext.error.mock.calls.length > 0) {
|
||||
throw new Error(mockContext.error.mock.calls[0][0]);
|
||||
}
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(typeof result).toBe('string');
|
||||
expect(result).toContain('export default');
|
||||
expect(result).toContain('project');
|
||||
expect(result).toContain('program');
|
||||
});
|
||||
|
||||
it('should return null for non-.yarnproject files', async () => {
|
||||
const plugin = yarnSpinnerVite();
|
||||
const loadFn = plugin.load as Function;
|
||||
|
||||
const result = await loadFn.call({}, 'some-file.ts');
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle errors by calling this.error', async () => {
|
||||
const plugin = yarnSpinnerVite();
|
||||
const loadFn = plugin.load as Function;
|
||||
|
||||
const mockThis = {
|
||||
error: vi.fn(),
|
||||
};
|
||||
|
||||
await loadFn.call(mockThis, 'nonexistent/.yarnproject');
|
||||
|
||||
expect(mockThis.error).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should accept loadOptions and pass them to loadYarnProject', async () => {
|
||||
const plugin = yarnSpinnerVite({
|
||||
loadOptions: {
|
||||
baseDir: resolve(__dirname, '../fixtures/simple'),
|
||||
},
|
||||
});
|
||||
const loadFn = plugin.load as Function;
|
||||
|
||||
const mockContext = {
|
||||
error: vi.fn(),
|
||||
};
|
||||
|
||||
const result = await loadFn.call(mockContext, projectPath);
|
||||
|
||||
expect(mockContext.error).not.toHaveBeenCalled();
|
||||
expect(result).toBeDefined();
|
||||
expect(typeof result).toBe('string');
|
||||
expect(result).toContain('export default');
|
||||
});
|
||||
|
||||
it('should have a transform function', () => {
|
||||
const plugin = yarnSpinnerVite();
|
||||
expect(plugin.transform).toBeDefined();
|
||||
expect(typeof plugin.transform).toBe('function');
|
||||
});
|
||||
|
||||
it('transform should return empty export for .yarnproject files', () => {
|
||||
const plugin = yarnSpinnerVite();
|
||||
const transformFn = plugin.transform as Function;
|
||||
|
||||
const result = transformFn.call({}, 'console.log("test")', projectPath);
|
||||
|
||||
expect(result).toEqual({
|
||||
code: 'export default {};',
|
||||
map: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('transform should return null for non-.yarnproject files', () => {
|
||||
const plugin = yarnSpinnerVite();
|
||||
const transformFn = plugin.transform as Function;
|
||||
|
||||
const result = transformFn.call({}, 'console.log("test")', 'some-file.ts');
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,98 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { YarnSpinnerWebpackLoader } from '../../src/plugins/webpack';
|
||||
import { resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
||||
|
||||
describe('YarnSpinnerWebpackLoader', () => {
|
||||
// Fixtures are in tests/fixtures/, not tests/plugins/fixtures/
|
||||
const projectPath = resolve(__dirname, '../fixtures/simple/.yarnproject');
|
||||
|
||||
it('should be a function', () => {
|
||||
expect(typeof YarnSpinnerWebpackLoader).toBe('function');
|
||||
});
|
||||
|
||||
it('should load .yarnproject files and return exported JSON', function () {
|
||||
const mockContext = {
|
||||
getOptions: vi.fn().mockReturnValue({}),
|
||||
resourcePath: projectPath,
|
||||
emitError: vi.fn(),
|
||||
};
|
||||
|
||||
const result = YarnSpinnerWebpackLoader.call(mockContext, '');
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(typeof result).toBe('string');
|
||||
expect(result).toContain('export default');
|
||||
expect(result).toContain('project');
|
||||
expect(result).toContain('program');
|
||||
// Ensure no errors were emitted
|
||||
expect(mockContext.emitError).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should pass loadOptions to loadYarnProjectSync', function () {
|
||||
const mockContext = {
|
||||
getOptions: vi.fn().mockReturnValue({
|
||||
loadOptions: {
|
||||
baseDir: resolve(__dirname, 'fixtures/simple'),
|
||||
},
|
||||
}),
|
||||
resourcePath: projectPath,
|
||||
emitError: vi.fn(),
|
||||
};
|
||||
|
||||
const result = YarnSpinnerWebpackLoader.call(mockContext, '');
|
||||
|
||||
expect(result).toContain('export default');
|
||||
expect(mockContext.emitError).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle errors by calling emitError', function () {
|
||||
const mockContext = {
|
||||
getOptions: vi.fn().mockReturnValue({}),
|
||||
resourcePath: 'nonexistent/.yarnproject',
|
||||
emitError: vi.fn(),
|
||||
};
|
||||
|
||||
const originalSource = '// original source';
|
||||
const result = YarnSpinnerWebpackLoader.call(mockContext, originalSource);
|
||||
|
||||
expect(mockContext.emitError).toHaveBeenCalled();
|
||||
expect(mockContext.emitError).toHaveBeenCalledWith(expect.any(Error));
|
||||
// Should return original source on error
|
||||
expect(result).toBe(originalSource);
|
||||
});
|
||||
|
||||
it('should return original source on error', function () {
|
||||
const originalSource = '// original source';
|
||||
const mockContext = {
|
||||
getOptions: vi.fn().mockReturnValue({}),
|
||||
resourcePath: 'nonexistent/.yarnproject',
|
||||
emitError: vi.fn(),
|
||||
};
|
||||
|
||||
const result = YarnSpinnerWebpackLoader.call(mockContext, originalSource);
|
||||
|
||||
expect(result).toBe(originalSource);
|
||||
});
|
||||
|
||||
it('should handle non-Error objects in error handling', function () {
|
||||
const mockContext = {
|
||||
getOptions: vi.fn().mockReturnValue({}),
|
||||
resourcePath: 'nonexistent/.yarnproject',
|
||||
emitError: vi.fn(),
|
||||
};
|
||||
|
||||
YarnSpinnerWebpackLoader.call(mockContext, '');
|
||||
|
||||
expect(mockContext.emitError).toHaveBeenCalled();
|
||||
const emittedError = mockContext.emitError.mock.calls[0][0];
|
||||
expect(emittedError).toBeInstanceOf(Error);
|
||||
});
|
||||
|
||||
it('should have a default export', () => {
|
||||
// Import using ESM to check default export
|
||||
expect(YarnSpinnerWebpackLoader).toBeDefined();
|
||||
});
|
||||
});
|
||||
+1
-1
@@ -10,7 +10,7 @@ export default defineConfig([
|
||||
outDir: 'dist',
|
||||
},
|
||||
{
|
||||
entry: ['src/node.ts'],
|
||||
entry: ['src/runner.ts'],
|
||||
format: ['esm', 'cjs'],
|
||||
dts: true,
|
||||
sourcemap: true,
|
||||
|
||||
Reference in New Issue
Block a user