diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..68aa397 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,33 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install dependencies + run: npm install + + - name: Type check + run: npm run typecheck + + - name: Lint + run: npm run lint + + - name: Build + run: npm run build + + - name: Test + run: npm run test:run diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..1c111dd --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,64 @@ +name: Release + +on: + push: + tags: + - 'v*' + workflow_dispatch: + inputs: + version: + description: 'WASM version (e.g., 0.0.46.1)' + required: true + t_ruby_version: + description: 't-ruby version (e.g., 0.0.46, defaults to latest)' + required: false + +permissions: + contents: write + id-token: write + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: '20' + registry-url: 'https://registry.npmjs.org' + + - name: Install dependencies + run: npm install + + - name: Extract version + id: version + run: | + if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then + VERSION="${{ inputs.version }}" + T_RUBY_VERSION="${{ inputs.t_ruby_version }}" + else + VERSION=${GITHUB_REF#refs/tags/v} + T_RUBY_VERSION="" + fi + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "t_ruby_version=$T_RUBY_VERSION" >> $GITHUB_OUTPUT + echo "Publishing version: $VERSION" + + - name: Build + run: npm run build + + - name: Update version + run: npm version ${{ steps.version.outputs.version }} --no-git-tag-version --allow-same-version + + - name: Publish to npm + run: npm publish --access public --provenance + + - name: Create Release + uses: softprops/action-gh-release@v2 + with: + name: "v${{ steps.version.outputs.version }}" + generate_release_notes: true + files: | + dist/* + package.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9cc55c3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,38 @@ +# Dependencies +node_modules/ + +# Build output +dist/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log +npm-debug.log* + +# Test coverage +coverage/ + +# Environment +.env +.env.local +.env.*.local + +# Temporary files +tmp/ +temp/ +*.tmp + +# Package manager locks (optional, uncomment if not using) +# package-lock.json +# yarn.lock +# pnpm-lock.yaml diff --git a/CONTRIBUTING.ko.md b/CONTRIBUTING.ko.md new file mode 100644 index 0000000..847cb28 --- /dev/null +++ b/CONTRIBUTING.ko.md @@ -0,0 +1,114 @@ +# T-Ruby WASM 기여 가이드 + +> [English Documentation](./CONTRIBUTING.md) + +T-Ruby WASM에 기여해 주셔서 감사합니다! 이 문서는 기여자를 위한 가이드라인과 정보를 제공합니다. + +## 행동 강령 + +모든 상호작용에서 존중과 건설적인 태도를 유지해 주세요. 모든 배경과 경험 수준의 기여자를 환영합니다. + +## 시작하기 + +1. 저장소를 포크하세요 +2. 포크한 저장소를 클론하세요: + ```bash + git clone https://github.com/YOUR_USERNAME/t-ruby-wasm.git + cd t-ruby-wasm + ``` +3. 의존성을 설치하세요: + ```bash + npm install + ``` +4. 변경사항을 위한 브랜치를 생성하세요: + ```bash + git checkout -b feature/your-feature-name + ``` + +## 개발 가이드라인 + +### 코드 스타일 + +- 모든 소스 파일에 TypeScript 사용 +- 기존 코드 스타일 준수 +- 각 파일은 하나의 export만 가져야 함 +- 파일은 100줄 미만으로 유지 (주석 포함) +- 종합적인 JSDoc 주석 작성 + +### SOLID 원칙 + +SOLID 원칙을 따릅니다: + +- **S**ingle Responsibility: 각 클래스/모듈은 하나의 책임만 +- **O**pen/Closed: 확장에 열려있고 수정에 닫혀있음 +- **L**iskov Substitution: 서브타입은 대체 가능해야 함 +- **I**nterface Segregation: 인터페이스를 작고 집중적으로 유지 +- **D**ependency Inversion: 추상화에 의존 + +### DRY 원칙 + +같은 코드를 반복하지 마세요. 공통 로직은 재사용 가능한 유틸리티로 추출하세요. + +## 테스팅 + +### 테스트 주도 개발 (TDD) + +TDD 방법론을 사용합니다: + +1. 먼저 실패하는 테스트 작성 +2. 테스트를 통과하는 최소한의 코드 작성 +3. 테스트를 통과 상태로 유지하면서 리팩토링 + +### 테스트 실행 + +```bash +# 테스트 실행 +npm test + +# 감시 모드로 테스트 실행 +npm run test:watch + +# 커버리지와 함께 테스트 실행 +npm run test:coverage +``` + +## Pull Request 과정 + +1. 모든 테스트가 통과하는지 확인 +2. 필요한 경우 문서 업데이트 +3. 새 기능에 대한 테스트 추가 +4. 커밋은 원자적이고 설명이 잘 되어야 함 +5. 관련 이슈 참조 + +### 커밋 메시지 형식 + +``` +type: 짧은 설명 + +필요한 경우 더 긴 설명. + +Fixes #123 +``` + +타입: `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore` + +## 프로젝트 구조 + +``` +t-ruby-wasm/ +├── src/ +│ ├── types/ # 타입 정의 (파일당 하나) +│ ├── vm/ # Ruby VM 관련 코드 +│ ├── utils/ # 유틸리티 함수 +│ ├── TRuby.ts # 메인 TRuby 클래스 +│ ├── createTRuby.ts # 팩토리 함수 +│ ├── VirtualFileSystem.ts +│ └── index.ts # 공개 exports +├── tests/ # 테스트 파일 +├── scripts/ # 빌드 스크립트 +└── dist/ # 빌드 출력 +``` + +## 질문이 있으신가요? + +질문이나 논의를 위해 이슈를 열어주세요. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..f92dd85 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,114 @@ +# Contributing to T-Ruby WASM + +> [한국어 문서](./CONTRIBUTING.ko.md) + +Thank you for your interest in contributing to T-Ruby WASM! This document provides guidelines and information for contributors. + +## Code of Conduct + +Please be respectful and constructive in all interactions. We welcome contributors of all backgrounds and experience levels. + +## Getting Started + +1. Fork the repository +2. Clone your fork: + ```bash + git clone https://github.com/YOUR_USERNAME/t-ruby-wasm.git + cd t-ruby-wasm + ``` +3. Install dependencies: + ```bash + npm install + ``` +4. Create a branch for your changes: + ```bash + git checkout -b feature/your-feature-name + ``` + +## Development Guidelines + +### Code Style + +- Use TypeScript for all source files +- Follow the existing code style +- Each file should have a single export +- Keep files under 100 lines (including comments) +- Write comprehensive JSDoc comments + +### SOLID Principles + +We follow SOLID principles: + +- **S**ingle Responsibility: Each class/module has one responsibility +- **O**pen/Closed: Open for extension, closed for modification +- **L**iskov Substitution: Subtypes must be substitutable +- **I**nterface Segregation: Keep interfaces small and focused +- **D**ependency Inversion: Depend on abstractions + +### DRY Principle + +Don't Repeat Yourself. Extract common logic into reusable utilities. + +## Testing + +### Test-Driven Development (TDD) + +We use TDD methodology: + +1. Write a failing test first +2. Write the minimum code to pass the test +3. Refactor while keeping tests green + +### Running Tests + +```bash +# Run tests +npm test + +# Run tests in watch mode +npm run test:watch + +# Run tests with coverage +npm run test:coverage +``` + +## Pull Request Process + +1. Ensure all tests pass +2. Update documentation if needed +3. Add tests for new functionality +4. Keep commits atomic and well-described +5. Reference any related issues + +### Commit Message Format + +``` +type: short description + +Longer description if needed. + +Fixes #123 +``` + +Types: `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore` + +## Project Structure + +``` +t-ruby-wasm/ +├── src/ +│ ├── types/ # Type definitions (one per file) +│ ├── vm/ # Ruby VM related code +│ ├── utils/ # Utility functions +│ ├── TRuby.ts # Main TRuby class +│ ├── createTRuby.ts # Factory function +│ ├── VirtualFileSystem.ts +│ └── index.ts # Public exports +├── tests/ # Test files +├── scripts/ # Build scripts +└── dist/ # Build output +``` + +## Questions? + +Feel free to open an issue for questions or discussions. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ffcde1e --- /dev/null +++ b/LICENSE @@ -0,0 +1,25 @@ +BSD 2-Clause License + +Copyright (c) 2024-2025, Y. Fred Kim +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.ko.md b/README.ko.md new file mode 100644 index 0000000..f81e22e --- /dev/null +++ b/README.ko.md @@ -0,0 +1,241 @@ +# T-Ruby WASM + +[![npm version](https://badge.fury.io/js/@t-ruby%2Fwasm.svg)](https://www.npmjs.com/package/@t-ruby/wasm) +[![License: BSD-2-Clause](https://img.shields.io/badge/License-BSD--2--Clause-blue.svg)](https://opensource.org/licenses/BSD-2-Clause) + +> [English Documentation](./README.md) + +브라우저 환경에서 WebAssembly로 실행되는 T-Ruby 컴파일러입니다. 이 패키지를 사용하면 브라우저에서 직접 T-Ruby(`.trb`) 파일을 Ruby(`.rb`)로 컴파일할 수 있어, 문서화 플레이그라운드, 온라인 에디터, 교육 도구 등에 적합합니다. + +## T-Ruby란? + +[T-Ruby](https://github.com/type-ruby/t-ruby)는 TypeScript에서 영감을 받은 Ruby용 타입 레이어입니다. 타입 어노테이션이 포함된 `.trb` 파일을 표준 `.rb` 파일로 컴파일하고 `.rbs` 타입 시그니처 파일을 생성합니다. 이 패키지는 그 기능을 WebAssembly를 통해 브라우저로 가져옵니다. + +## 설치 + +```bash +npm install @t-ruby/wasm +``` + +## 빠른 시작 + +```typescript +import { createTRuby } from '@t-ruby/wasm'; + +// T-Ruby WASM 런타임 초기화 +const tRuby = await createTRuby(); + +// T-Ruby 코드 컴파일 +const result = await tRuby.compile(` + def greet(name: String): String + "Hello, #{name}!" + end +`); + +if (result.success) { + console.log(result.ruby); // 컴파일된 Ruby 코드 + console.log(result.rbs); // 생성된 RBS 시그니처 +} else { + console.error(result.errors); +} +``` + +## API 레퍼런스 + +### `createTRuby(options?)` + +T-Ruby 인스턴스를 생성하고 초기화합니다. 시작하는 데 권장되는 방법입니다. + +```typescript +const tRuby = await createTRuby({ + debug: false, // 디버그 로깅 활성화 +}); +``` + +### `TRuby` 클래스 + +더 세밀한 제어가 필요한 경우 `TRuby` 클래스를 직접 사용할 수 있습니다. + +```typescript +import { TRuby } from '@t-ruby/wasm'; + +const tRuby = new TRuby(); +await tRuby.initialize(); +``` + +#### `compile(source, filename?)` + +T-Ruby 소스 코드를 Ruby로 컴파일합니다. + +```typescript +const result = await tRuby.compile(` + interface Greeter + def greet(name: String): String + end + + class HelloGreeter + implements Greeter + + def greet(name: String): String + "Hello, #{name}!" + end + end +`, 'greeter.trb'); + +console.log(result.ruby); // 컴파일된 Ruby 코드 +console.log(result.rbs); // RBS 타입 시그니처 +console.log(result.errors); // 컴파일 에러 (있는 경우) +console.log(result.warnings); // 컴파일 경고 (있는 경우) +``` + +#### `typeCheck(source, filename?)` + +컴파일 없이 T-Ruby 소스 코드의 타입을 검사합니다. + +```typescript +const result = await tRuby.typeCheck(` + def add(a: Integer, b: Integer): Integer + a + b + end + + add("not", "integers") +`); + +if (!result.valid) { + result.errors?.forEach(err => { + console.error(`${err.line}:${err.column} - ${err.message}`); + }); +} +``` + +#### 가상 파일 시스템 + +다중 파일 프로젝트를 위해 가상 파일 시스템의 파일을 관리합니다. + +```typescript +// 개별 파일 추가 +tRuby.addFile('lib/utils.trb', ` + def helper(x: Integer): Integer + x * 2 + end +`); + +// 여러 파일 추가 +tRuby.addFiles([ + { path: 'lib/models.trb', content: '# models' }, + { path: 'lib/services.trb', content: '# services' }, +]); + +// 모든 파일 가져오기 +const files = tRuby.getFiles(); + +// 파일 제거 +tRuby.removeFile('lib/utils.trb'); + +// 모든 파일 지우기 +tRuby.clearFiles(); +``` + +#### `getVersion()` + +버전 정보를 가져옵니다. + +```typescript +const versions = await tRuby.getVersion(); +console.log(versions.tRuby); // T-Ruby 버전 +console.log(versions.ruby); // Ruby 버전 +console.log(versions.rubyWasm); // Ruby WASM 버전 +``` + +#### `eval(code)` + +임의의 Ruby 코드를 실행합니다 (고급 사용 사례용). + +```typescript +const result = await tRuby.eval('1 + 2 + 3'); +console.log(result); // 6 +``` + +## 타입 + +패키지는 다음 TypeScript 타입을 내보냅니다: + +```typescript +import type { + TRubyOptions, + CompileResult, + CompileError, + CompileWarning, + TypeCheckResult, + TypeCheckError, + VirtualFile, + VersionInfo, +} from '@t-ruby/wasm'; +``` + +## 브라우저 사용법 + +### 번들러 사용 (Vite, Webpack 등) + +```typescript +import { createTRuby } from '@t-ruby/wasm'; + +async function main() { + const tRuby = await createTRuby(); + // tRuby 사용... +} + +main(); +``` + +### CDN 사용 (ESM) + +```html + +``` + +## 사용 사례 + +- **문서화 플레이그라운드**: 사용자가 문서에서 직접 T-Ruby 코드를 시험해볼 수 있게 함 +- **온라인 에디터**: 브라우저 기반 T-Ruby 에디터 구축 +- **교육 도구**: T-Ruby 학습을 위한 대화형 튜토리얼 제작 +- **코드 미리보기**: 사용자가 입력하는 대로 실시간으로 컴파일 출력 표시 + +## 요구 사항 + +- WebAssembly를 지원하는 최신 브라우저 +- Node.js 18+ (개발/빌드용) + +## 개발 + +```bash +# 의존성 설치 +npm install + +# 패키지 빌드 +npm run build + +# 테스트 실행 +npm test + +# 타입 검사 +npm run typecheck + +# 린트 +npm run lint +``` + +## 라이선스 + +BSD-2-Clause + +## 관련 프로젝트 + +- [T-Ruby](https://github.com/type-ruby/t-ruby) - T-Ruby 컴파일러 (Ruby) +- [ruby.wasm](https://github.com/ruby/ruby.wasm) - WebAssembly의 Ruby diff --git a/README.md b/README.md index 80b49f1..e06f16e 100644 --- a/README.md +++ b/README.md @@ -1 +1,243 @@ -# t-ruby-wasm +# T-Ruby WASM + +[![npm version](https://badge.fury.io/js/@t-ruby%2Fwasm.svg)](https://www.npmjs.com/package/@t-ruby/wasm) +[![License: BSD-2-Clause](https://img.shields.io/badge/License-BSD--2--Clause-blue.svg)](https://opensource.org/licenses/BSD-2-Clause) + +> [Korean Documentation (한국어 문서)](./README.ko.md) + +T-Ruby compiler running in WebAssembly for browser environments. This package allows you to compile T-Ruby (`.trb`) files to Ruby (`.rb`) directly in the browser, making it perfect for documentation playgrounds, online editors, and educational tools. + +## What is T-Ruby? + +[T-Ruby](https://github.com/type-ruby/t-ruby) is a TypeScript-inspired typed layer for Ruby. It compiles `.trb` files with type annotations into standard `.rb` files and generates `.rbs` type signature files. This package brings that functionality to the browser via WebAssembly. + +## Installation + +```bash +npm install @t-ruby/wasm +``` + +## Quick Start + +```typescript +import { createTRuby } from '@t-ruby/wasm'; + +// Initialize the T-Ruby WASM runtime +const tRuby = await createTRuby(); + +// Compile T-Ruby code +const result = await tRuby.compile(` + def greet(name: String): String + "Hello, #{name}!" + end +`); + +if (result.success) { + console.log(result.ruby); // Compiled Ruby code + console.log(result.rbs); // Generated RBS signatures +} else { + console.error(result.errors); +} +``` + +## API Reference + +### `createTRuby(options?)` + +Creates and initializes a T-Ruby instance. This is the recommended way to get started. + +```typescript +const tRuby = await createTRuby({ + debug: false, // Enable debug logging + stdout: (text) => console.log(text), // Custom stdout handler + stderr: (text) => console.error(text), // Custom stderr handler +}); +``` + +### `TRuby` Class + +For more control, you can use the `TRuby` class directly. + +```typescript +import { TRuby } from '@t-ruby/wasm'; + +const tRuby = new TRuby(); +await tRuby.initialize(); +``` + +#### `compile(source, filename?)` + +Compiles T-Ruby source code to Ruby. + +```typescript +const result = await tRuby.compile(` + interface Greeter + def greet(name: String): String + end + + class HelloGreeter + implements Greeter + + def greet(name: String): String + "Hello, #{name}!" + end + end +`, 'greeter.trb'); + +console.log(result.ruby); // Compiled Ruby code +console.log(result.rbs); // RBS type signatures +console.log(result.errors); // Compilation errors (if any) +console.log(result.warnings); // Compilation warnings (if any) +``` + +#### `typeCheck(source, filename?)` + +Type checks T-Ruby source code without compiling. + +```typescript +const result = await tRuby.typeCheck(` + def add(a: Integer, b: Integer): Integer + a + b + end + + add("not", "integers") +`); + +if (!result.valid) { + result.errors?.forEach(err => { + console.error(`${err.line}:${err.column} - ${err.message}`); + }); +} +``` + +#### Virtual File System + +Manage files in the virtual file system for multi-file projects. + +```typescript +// Add individual files +tRuby.addFile('lib/utils.trb', ` + def helper(x: Integer): Integer + x * 2 + end +`); + +// Add multiple files +tRuby.addFiles([ + { path: 'lib/models.trb', content: '# models' }, + { path: 'lib/services.trb', content: '# services' }, +]); + +// Get all files +const files = tRuby.getFiles(); + +// Remove a file +tRuby.removeFile('lib/utils.trb'); + +// Clear all files +tRuby.clearFiles(); +``` + +#### `getVersion()` + +Get version information. + +```typescript +const versions = await tRuby.getVersion(); +console.log(versions.tRuby); // T-Ruby version +console.log(versions.ruby); // Ruby version +console.log(versions.rubyWasm); // Ruby WASM version +``` + +#### `eval(code)` + +Execute arbitrary Ruby code (for advanced use cases). + +```typescript +const result = await tRuby.eval('1 + 2 + 3'); +console.log(result); // 6 +``` + +## Types + +The package exports the following TypeScript types: + +```typescript +import type { + TRubyOptions, + CompileResult, + CompileError, + CompileWarning, + TypeCheckResult, + TypeCheckError, + VirtualFile, + VersionInfo, +} from '@t-ruby/wasm'; +``` + +## Browser Usage + +### With a Bundler (Vite, Webpack, etc.) + +```typescript +import { createTRuby } from '@t-ruby/wasm'; + +async function main() { + const tRuby = await createTRuby(); + // Use tRuby... +} + +main(); +``` + +### Via CDN (ESM) + +```html + +``` + +## Use Cases + +- **Documentation Playgrounds**: Let users try T-Ruby code directly in your docs +- **Online Editors**: Build browser-based T-Ruby editors +- **Educational Tools**: Create interactive tutorials for learning T-Ruby +- **Code Preview**: Show compiled output in real-time as users type + +## Requirements + +- Modern browser with WebAssembly support +- Node.js 18+ (for development/build) + +## Development + +```bash +# Install dependencies +npm install + +# Build the package +npm run build + +# Run tests +npm test + +# Type check +npm run typecheck + +# Lint +npm run lint +``` + +## License + +BSD-2-Clause + +## Related Projects + +- [T-Ruby](https://github.com/type-ruby/t-ruby) - The T-Ruby compiler (Ruby) +- [ruby.wasm](https://github.com/ruby/ruby.wasm) - Ruby in WebAssembly diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..9da81d2 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,16 @@ +import js from "@eslint/js"; +import tseslint from "typescript-eslint"; + +export default tseslint.config( + js.configs.recommended, + ...tseslint.configs.recommended, + { + ignores: ["dist/**", "node_modules/**", "*.cjs"], + }, + { + files: ["src/**/*.ts"], + rules: { + "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }], + }, + } +); diff --git a/package.json b/package.json new file mode 100644 index 0000000..13016c0 --- /dev/null +++ b/package.json @@ -0,0 +1,69 @@ +{ + "name": "@t-ruby/wasm", + "version": "0.0.46", + "description": "T-Ruby compiler running in WebAssembly for browser environments", + "type": "module", + "main": "dist/index.cjs", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + ".": { + "import": "./dist/index.js", + "require": "./dist/index.cjs", + "types": "./dist/index.d.ts" + } + }, + "files": [ + "dist", + "README.md", + "README.ko.md" + ], + "scripts": { + "build": "tsup", + "build:wasm": "node scripts/build-wasm.mjs", + "dev": "tsup --watch", + "typecheck": "tsc --noEmit", + "lint": "eslint src", + "test": "vitest", + "test:run": "vitest run", + "prepublishOnly": "npm run build" + }, + "keywords": [ + "t-ruby", + "ruby", + "typescript", + "wasm", + "webassembly", + "type-checker", + "compiler" + ], + "author": "Y. Fred Kim ", + "license": "BSD-2-Clause", + "repository": { + "type": "git", + "url": "https://github.com/type-ruby/t-ruby-wasm.git" + }, + "homepage": "https://type-ruby.github.io", + "bugs": { + "url": "https://github.com/type-ruby/t-ruby-wasm/issues" + }, + "engines": { + "node": ">=18.0.0" + }, + "devDependencies": { + "@eslint/js": "^9.18.0", + "@types/node": "^22.10.7", + "eslint": "^9.18.0", + "tsup": "^8.3.5", + "typescript": "^5.7.3", + "typescript-eslint": "^8.20.0", + "vitest": "^3.0.2" + }, + "dependencies": { + "@ruby/3.4-wasm-wasi": "^2.8.1", + "@ruby/wasm-wasi": "^2.8.1" + }, + "t-ruby": { + "minVersion": "0.0.46" + } +} diff --git a/scripts/build-wasm.mjs b/scripts/build-wasm.mjs new file mode 100644 index 0000000..7335e18 --- /dev/null +++ b/scripts/build-wasm.mjs @@ -0,0 +1,44 @@ +#!/usr/bin/env node + +/** + * Build script for T-Ruby WASM + * + * This script downloads and bundles the T-Ruby gem into the WASM distribution. + * In production, this would fetch the gem from RubyGems and package it + * with the Ruby WASM runtime. + */ + +import { execSync } from "node:child_process"; +import { existsSync, mkdirSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const rootDir = join(__dirname, ".."); +const distDir = join(rootDir, "dist"); + +console.log("Building T-Ruby WASM..."); + +// Ensure dist directory exists +if (!existsSync(distDir)) { + mkdirSync(distDir, { recursive: true }); +} + +// Build TypeScript +console.log("Compiling TypeScript..."); +try { + execSync("npm run build", { cwd: rootDir, stdio: "inherit" }); + console.log("TypeScript compilation complete."); +} catch (error) { + console.error("TypeScript compilation failed:", error.message); + process.exit(1); +} + +console.log("T-Ruby WASM build complete!"); +console.log(""); +console.log("To use in your project:"); +console.log(" npm install @t-ruby/wasm"); +console.log(""); +console.log("Then import and use:"); +console.log(" import { createTRuby } from '@t-ruby/wasm';"); +console.log(" const tRuby = await createTRuby();"); diff --git a/src/TRuby.ts b/src/TRuby.ts new file mode 100644 index 0000000..9842e84 --- /dev/null +++ b/src/TRuby.ts @@ -0,0 +1,92 @@ +import type { TRubyOptions } from "./types/TRubyOptions.js"; +import type { CompileResult } from "./types/CompileResult.js"; +import type { TypeCheckResult } from "./types/TypeCheckResult.js"; +import type { VersionInfo } from "./types/VersionInfo.js"; +import type { VirtualFile } from "./types/VirtualFile.js"; +import type { RubyVM } from "./vm/RubyVM.js"; +import { VirtualFileSystem } from "./VirtualFileSystem.js"; +import { escapeRubyString } from "./utils/escapeRubyString.js"; +import { T_RUBY_INIT_SCRIPT } from "./vm/TRubyInitScript.js"; +import { RUBY_WASM_VERSION } from "./constants.js"; + +/** T-Ruby WASM wrapper for compiling T-Ruby to Ruby in the browser */ +export class TRuby { + private vm: RubyVM | null = null; + private initialized = false; + private vfs = new VirtualFileSystem(); + + constructor(_options: TRubyOptions = {}) {} + + /** Initialize the T-Ruby WASM runtime. Must be called first. */ + async initialize(): Promise { + if (this.initialized) return; + this.vm = await this.loadWasm(); + await this.vm.evalAsync(T_RUBY_INIT_SCRIPT); + this.initialized = true; + } + + /** Compile T-Ruby source code to Ruby */ + async compile(source: string, filename = "input.trb"): Promise { + this.ensureInit(); + try { + const r = await this.evalJson( + `TRuby::Compiler.compile(${escapeRubyString(source)}, filename: ${escapeRubyString(filename)}).to_json` + ); + return { ...r, success: !r.errors?.length }; + } catch (e) { + return { success: false, errors: [{ message: String(e) }] }; + } + } + + /** Type check T-Ruby source code */ + async typeCheck(source: string, filename = "input.trb"): Promise { + this.ensureInit(); + try { + return await this.evalJson( + `TRuby::TypeChecker.check(${escapeRubyString(source)}, filename: ${escapeRubyString(filename)}).to_json` + ); + } catch (e) { + return { valid: false, errors: [{ message: String(e) }] }; + } + } + + addFile(path: string, content: string): void { this.vfs.addFile(path, content); } + addFiles(files: VirtualFile[]): void { this.vfs.addFiles(files); } + removeFile(path: string): void { this.vfs.removeFile(path); } + clearFiles(): void { this.vfs.clear(); } + getFiles(): Map { return this.vfs.getAllFiles(); } + + /** Get version information */ + async getVersion(): Promise { + this.ensureInit(); + const [tRuby, ruby] = await Promise.all([ + this.vm!.evalAsync("TRuby::VERSION") as Promise, + this.vm!.evalAsync("RUBY_VERSION") as Promise, + ]); + return { tRuby, ruby, rubyWasm: RUBY_WASM_VERSION }; + } + + /** Execute arbitrary Ruby code */ + async eval(code: string): Promise { + this.ensureInit(); + return this.vm!.evalAsync(code); + } + + isInitialized(): boolean { return this.initialized; } + + private async loadWasm(): Promise { + const { DefaultRubyVM } = await import("@ruby/wasm-wasi/dist/browser"); + const url = new URL("@ruby/3.4-wasm-wasi/dist/ruby+stdlib.wasm", import.meta.url); + const mod = await WebAssembly.compileStreaming(fetch(url)); + const result = await DefaultRubyVM(mod); + return result.vm as RubyVM; + } + + private async evalJson(code: string): Promise { + return JSON.parse(await this.vm!.evalAsync(`require 'json'; ${code}`) as string); + } + + private ensureInit(): void { + if (!this.initialized) throw new Error("T-Ruby WASM not initialized. Call initialize() first."); + } +} diff --git a/src/VirtualFileSystem.ts b/src/VirtualFileSystem.ts new file mode 100644 index 0000000..b4698f8 --- /dev/null +++ b/src/VirtualFileSystem.ts @@ -0,0 +1,61 @@ +import type { VirtualFile } from "./types/VirtualFile.js"; + +/** + * In-memory file system for managing T-Ruby source files + * + * @remarks + * Provides a virtual file system for browser environments where + * there's no access to the real file system. Useful for multi-file + * T-Ruby projects in playgrounds and online editors. + * + * @example + * ```typescript + * const vfs = new VirtualFileSystem(); + * vfs.addFile("lib/utils.trb", "def helper: Integer\n 42\nend"); + * ``` + */ +export class VirtualFileSystem { + private files: Map = new Map(); + + /** Add a file to the virtual file system */ + addFile(path: string, content: string): void { + this.files.set(path, content); + } + + /** Add multiple files at once */ + addFiles(files: VirtualFile[]): void { + for (const file of files) { + this.addFile(file.path, file.content); + } + } + + /** Get file content by path */ + getFile(path: string): string | undefined { + return this.files.get(path); + } + + /** Check if a file exists */ + hasFile(path: string): boolean { + return this.files.has(path); + } + + /** Remove a file from the virtual file system */ + removeFile(path: string): boolean { + return this.files.delete(path); + } + + /** Get all files as a Map */ + getAllFiles(): Map { + return new Map(this.files); + } + + /** Clear all files from the virtual file system */ + clear(): void { + this.files.clear(); + } + + /** Get the number of files in the virtual file system */ + get size(): number { + return this.files.size; + } +} diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 0000000..a89046c --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,8 @@ +/** + * Package constants for T-Ruby WASM + * + * @internal + */ + +/** Current Ruby WASM runtime version */ +export const RUBY_WASM_VERSION = "2.7.0"; diff --git a/src/createTRuby.ts b/src/createTRuby.ts new file mode 100644 index 0000000..a321ca1 --- /dev/null +++ b/src/createTRuby.ts @@ -0,0 +1,30 @@ +import type { TRubyOptions } from "./types/TRubyOptions.js"; +import { TRuby } from "./TRuby.js"; + +/** + * Create and initialize a T-Ruby instance + * + * @remarks + * This is a convenience function that creates a new TRuby instance + * and initializes it in one step. Recommended for most use cases. + * + * @param options - Optional configuration options + * @returns Promise resolving to an initialized TRuby instance + * + * @example + * ```typescript + * import { createTRuby } from '@t-ruby/wasm'; + * + * const tRuby = await createTRuby(); + * const result = await tRuby.compile(` + * def hello(name: String): String + * "Hello, #{name}!" + * end + * `); + * ``` + */ +export async function createTRuby(options?: TRubyOptions): Promise { + const instance = new TRuby(options); + await instance.initialize(); + return instance; +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..f5b6cd7 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,20 @@ +/** + * T-Ruby WASM + * + * A WebAssembly-based T-Ruby compiler for browser environments. + * + * @packageDocumentation + */ + +export { TRuby } from "./TRuby.js"; +export { createTRuby } from "./createTRuby.js"; +export { VirtualFileSystem } from "./VirtualFileSystem.js"; + +export type { TRubyOptions } from "./types/TRubyOptions.js"; +export type { CompileResult } from "./types/CompileResult.js"; +export type { CompileError } from "./types/CompileError.js"; +export type { CompileWarning } from "./types/CompileWarning.js"; +export type { TypeCheckResult } from "./types/TypeCheckResult.js"; +export type { TypeCheckError } from "./types/TypeCheckError.js"; +export type { VirtualFile } from "./types/VirtualFile.js"; +export type { VersionInfo } from "./types/VersionInfo.js"; diff --git a/src/types/CompileError.ts b/src/types/CompileError.ts new file mode 100644 index 0000000..a35ad00 --- /dev/null +++ b/src/types/CompileError.ts @@ -0,0 +1,30 @@ +/** + * Represents a compilation error from the T-Ruby compiler + * + * @remarks + * Compilation errors occur when the source code contains syntax errors + * or other issues that prevent successful compilation. + * + * @example + * ```typescript + * const error: CompileError = { + * message: "Unexpected token", + * line: 10, + * column: 5, + * code: "E001" + * }; + * ``` + */ +export interface CompileError { + /** Human-readable error message describing the issue */ + message: string; + + /** Line number where the error occurred (1-indexed) */ + line?: number; + + /** Column number where the error occurred (1-indexed) */ + column?: number; + + /** Machine-readable error code for programmatic handling */ + code?: string; +} diff --git a/src/types/CompileResult.ts b/src/types/CompileResult.ts new file mode 100644 index 0000000..0a26cb2 --- /dev/null +++ b/src/types/CompileResult.ts @@ -0,0 +1,35 @@ +import type { CompileError } from "./CompileError.js"; +import type { CompileWarning } from "./CompileWarning.js"; + +/** + * Result of compiling a T-Ruby (.trb) file + * + * @remarks + * Contains the compiled Ruby code, generated RBS signatures, + * and any errors or warnings encountered during compilation. + * + * @example + * ```typescript + * const result: CompileResult = { + * success: true, + * ruby: "def greet(name)\n \"Hello, \#{name}!\"\nend", + * rbs: "def greet: (String name) -> String" + * }; + * ``` + */ +export interface CompileResult { + /** Whether the compilation completed without errors */ + success: boolean; + + /** Compiled Ruby code (only present if successful) */ + ruby?: string; + + /** Generated RBS type signature (only present if successful) */ + rbs?: string; + + /** Array of compilation errors (empty if successful) */ + errors?: CompileError[]; + + /** Array of compilation warnings (may exist even if successful) */ + warnings?: CompileWarning[]; +} diff --git a/src/types/CompileWarning.ts b/src/types/CompileWarning.ts new file mode 100644 index 0000000..3790aa6 --- /dev/null +++ b/src/types/CompileWarning.ts @@ -0,0 +1,30 @@ +/** + * Represents a compilation warning from the T-Ruby compiler + * + * @remarks + * Warnings indicate potential issues that don't prevent compilation + * but may cause unexpected behavior at runtime. + * + * @example + * ```typescript + * const warning: CompileWarning = { + * message: "Unused variable 'x'", + * line: 15, + * column: 3, + * code: "W001" + * }; + * ``` + */ +export interface CompileWarning { + /** Human-readable warning message */ + message: string; + + /** Line number where the warning occurred (1-indexed) */ + line?: number; + + /** Column number where the warning occurred (1-indexed) */ + column?: number; + + /** Machine-readable warning code for programmatic handling */ + code?: string; +} diff --git a/src/types/TRubyOptions.ts b/src/types/TRubyOptions.ts new file mode 100644 index 0000000..d03e6f3 --- /dev/null +++ b/src/types/TRubyOptions.ts @@ -0,0 +1,35 @@ +/** + * Options for initializing T-Ruby WASM instance + * + * @remarks + * These options allow customization of the T-Ruby runtime behavior, + * including debug output and custom I/O handlers. + * + * @example + * ```typescript + * const options: TRubyOptions = { + * debug: true, + * stdout: (text) => myLogger.info(text), + * stderr: (text) => myLogger.error(text), + * }; + * ``` + */ +export interface TRubyOptions { + /** + * Whether to print debug information to console + * @default false + */ + debug?: boolean; + + /** + * Custom stdout handler for Ruby output + * @param text - The text written to stdout + */ + stdout?: (text: string) => void; + + /** + * Custom stderr handler for Ruby errors + * @param text - The text written to stderr + */ + stderr?: (text: string) => void; +} diff --git a/src/types/TypeCheckError.ts b/src/types/TypeCheckError.ts new file mode 100644 index 0000000..36f2a8a --- /dev/null +++ b/src/types/TypeCheckError.ts @@ -0,0 +1,34 @@ +/** + * Represents a type checking error from the T-Ruby type checker + * + * @remarks + * Type errors occur when the code violates type constraints, + * such as passing wrong types to functions or assigning incompatible values. + * + * @example + * ```typescript + * const error: TypeCheckError = { + * message: "Type mismatch in argument", + * line: 20, + * column: 10, + * expected: "Integer", + * actual: "String" + * }; + * ``` + */ +export interface TypeCheckError { + /** Human-readable error message describing the type violation */ + message: string; + + /** Line number where the error occurred (1-indexed) */ + line?: number; + + /** Column number where the error occurred (1-indexed) */ + column?: number; + + /** The expected type at this location */ + expected?: string; + + /** The actual type found at this location */ + actual?: string; +} diff --git a/src/types/TypeCheckResult.ts b/src/types/TypeCheckResult.ts new file mode 100644 index 0000000..88120aa --- /dev/null +++ b/src/types/TypeCheckResult.ts @@ -0,0 +1,29 @@ +import type { TypeCheckError } from "./TypeCheckError.js"; + +/** + * Result of type checking T-Ruby source code + * + * @remarks + * Contains information about whether the type check passed + * and any type errors found in the source code. + * + * @example + * ```typescript + * const result: TypeCheckResult = { + * valid: false, + * errors: [{ + * message: "Cannot assign String to Integer", + * line: 5, + * expected: "Integer", + * actual: "String" + * }] + * }; + * ``` + */ +export interface TypeCheckResult { + /** Whether the type check passed without errors */ + valid: boolean; + + /** Array of type errors found (empty if valid is true) */ + errors?: TypeCheckError[]; +} diff --git a/src/types/VersionInfo.ts b/src/types/VersionInfo.ts new file mode 100644 index 0000000..8b6fcdf --- /dev/null +++ b/src/types/VersionInfo.ts @@ -0,0 +1,26 @@ +/** + * Version information for T-Ruby WASM components + * + * @remarks + * Provides version details for debugging and compatibility checks. + * Useful when reporting issues or verifying runtime environment. + * + * @example + * ```typescript + * const versions: VersionInfo = { + * tRuby: "1.0.0", + * rubyWasm: "2.7.0", + * ruby: "3.4.0" + * }; + * ``` + */ +export interface VersionInfo { + /** T-Ruby compiler version */ + tRuby: string; + + /** Ruby WASM runtime version */ + rubyWasm: string; + + /** Underlying Ruby interpreter version */ + ruby: string; +} diff --git a/src/types/VirtualFile.ts b/src/types/VirtualFile.ts new file mode 100644 index 0000000..1e11904 --- /dev/null +++ b/src/types/VirtualFile.ts @@ -0,0 +1,22 @@ +/** + * Represents a file in the virtual file system + * + * @remarks + * Virtual files are used to manage multi-file T-Ruby projects + * in the browser environment where there's no real file system. + * + * @example + * ```typescript + * const file: VirtualFile = { + * path: "lib/models/user.trb", + * content: "class User\n attr_reader name: String\nend" + * }; + * ``` + */ +export interface VirtualFile { + /** File path relative to the project root */ + path: string; + + /** File content as a string */ + content: string; +} diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 0000000..1d4fea9 --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1,23 @@ +/** + * T-Ruby WASM Type Definitions + * + * @remarks + * This module re-exports all type definitions for convenient importing. + * Each type is defined in its own file following the single-export pattern. + * + * @example + * ```typescript + * import type { TRubyOptions, CompileResult } from '@t-ruby/wasm'; + * ``` + * + * @packageDocumentation + */ + +export type { TRubyOptions } from "./TRubyOptions.js"; +export type { CompileError } from "./CompileError.js"; +export type { CompileWarning } from "./CompileWarning.js"; +export type { CompileResult } from "./CompileResult.js"; +export type { TypeCheckError } from "./TypeCheckError.js"; +export type { TypeCheckResult } from "./TypeCheckResult.js"; +export type { VirtualFile } from "./VirtualFile.js"; +export type { VersionInfo } from "./VersionInfo.js"; diff --git a/src/utils/escapeRubyString.ts b/src/utils/escapeRubyString.ts new file mode 100644 index 0000000..7988971 --- /dev/null +++ b/src/utils/escapeRubyString.ts @@ -0,0 +1,29 @@ +/** + * Escape a string for safe inclusion in Ruby code + * + * @remarks + * This function escapes special characters in a string so it can be + * safely interpolated into Ruby code without causing syntax errors + * or injection vulnerabilities. + * + * @param str - The string to escape + * @returns The escaped string wrapped in double quotes + * + * @example + * ```typescript + * const escaped = escapeRubyString('Hello\n"World"'); + * // Returns: "Hello\\n\\"World\\"" + * ``` + * + * @internal + */ +export function escapeRubyString(str: string): string { + const escaped = str + .replace(/\\/g, "\\\\") + .replace(/"/g, '\\"') + .replace(/\n/g, "\\n") + .replace(/\r/g, "\\r") + .replace(/\t/g, "\\t"); + + return `"${escaped}"`; +} diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..8bf2e14 --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,6 @@ +/** + * Utility functions + * @internal + */ + +export { escapeRubyString } from "./escapeRubyString.js"; diff --git a/src/vm/DefaultRubyVM.ts b/src/vm/DefaultRubyVM.ts new file mode 100644 index 0000000..58c062b --- /dev/null +++ b/src/vm/DefaultRubyVM.ts @@ -0,0 +1,15 @@ +import type { RubyVM } from "./RubyVM.js"; + +/** + * Interface for the default Ruby VM factory from ruby-wasm-wasi + * + * @remarks + * This interface represents the result of calling DefaultRubyVM() + * from the ruby-wasm-wasi package. + * + * @internal + */ +export interface DefaultRubyVMResult { + /** The initialized Ruby VM instance */ + vm: RubyVM; +} diff --git a/src/vm/RubyVM.ts b/src/vm/RubyVM.ts new file mode 100644 index 0000000..0b5dc78 --- /dev/null +++ b/src/vm/RubyVM.ts @@ -0,0 +1,24 @@ +/** + * Interface for the Ruby Virtual Machine from ruby-wasm-wasi + * + * @remarks + * This interface abstracts the Ruby VM provided by the ruby-wasm-wasi package. + * It enables running Ruby code in WebAssembly environments. + * + * @internal + */ +export interface RubyVM { + /** + * Evaluate Ruby code synchronously + * @param code - Ruby code to evaluate + * @returns Result of the evaluation + */ + eval(code: string): unknown; + + /** + * Evaluate Ruby code asynchronously + * @param code - Ruby code to evaluate + * @returns Promise resolving to the result + */ + evalAsync(code: string): Promise; +} diff --git a/src/vm/TRubyInitScript.ts b/src/vm/TRubyInitScript.ts new file mode 100644 index 0000000..97f6205 --- /dev/null +++ b/src/vm/TRubyInitScript.ts @@ -0,0 +1,38 @@ +/** + * Ruby initialization script for T-Ruby in WASM environment + * + * @remarks + * This script is executed when the Ruby VM is initialized. + * It attempts to load the T-Ruby gem, and falls back to a minimal + * implementation if the gem is not available. + * + * @internal + */ +export const T_RUBY_INIT_SCRIPT = ` +require 'rubygems' +begin + require 't-ruby' +rescue LoadError + # T-Ruby gem not available, use minimal implementation + module TRuby + VERSION = "0.0.0" + + class Compiler + def self.compile(source, filename: "input.trb") + # Minimal type erasure: strip type annotations + ruby_code = source.gsub(/:\\s*[A-Z][A-Za-z0-9_]*(?:<[^>]+>)?/, '') + ruby_code = ruby_code.gsub(/->\\s*[A-Z][A-Za-z0-9_]*(?:<[^>]+>)?/, '') + ruby_code = ruby_code.gsub(/^\\s*interface\\s+.*?^\\s*end/m, '') + ruby_code = ruby_code.gsub(/^\\s*type\\s+.*$/, '') + { ruby: ruby_code, rbs: "", errors: [], warnings: [] } + end + end + + class TypeChecker + def self.check(source, filename: "input.trb") + { valid: true, errors: [] } + end + end + end +end +`; diff --git a/src/vm/index.ts b/src/vm/index.ts new file mode 100644 index 0000000..ac0d2a8 --- /dev/null +++ b/src/vm/index.ts @@ -0,0 +1,8 @@ +/** + * Ruby VM related exports + * @internal + */ + +export type { RubyVM } from "./RubyVM.js"; +export type { DefaultRubyVMResult } from "./DefaultRubyVM.js"; +export { T_RUBY_INIT_SCRIPT } from "./TRubyInitScript.js"; diff --git a/tests/VirtualFileSystem.test.ts b/tests/VirtualFileSystem.test.ts new file mode 100644 index 0000000..23b4cb2 --- /dev/null +++ b/tests/VirtualFileSystem.test.ts @@ -0,0 +1,77 @@ +import { describe, it, expect, beforeEach } from "vitest"; +import { VirtualFileSystem } from "../src/VirtualFileSystem.js"; + +describe("VirtualFileSystem", () => { + let vfs: VirtualFileSystem; + + beforeEach(() => { + vfs = new VirtualFileSystem(); + }); + + describe("addFile", () => { + it("should add a file", () => { + vfs.addFile("test.trb", "content"); + expect(vfs.getFile("test.trb")).toBe("content"); + }); + + it("should overwrite existing file", () => { + vfs.addFile("test.trb", "old"); + vfs.addFile("test.trb", "new"); + expect(vfs.getFile("test.trb")).toBe("new"); + }); + }); + + describe("addFiles", () => { + it("should add multiple files", () => { + vfs.addFiles([ + { path: "a.trb", content: "a" }, + { path: "b.trb", content: "b" }, + ]); + expect(vfs.size).toBe(2); + }); + }); + + describe("hasFile", () => { + it("should return true for existing file", () => { + vfs.addFile("test.trb", "content"); + expect(vfs.hasFile("test.trb")).toBe(true); + }); + + it("should return false for non-existing file", () => { + expect(vfs.hasFile("missing.trb")).toBe(false); + }); + }); + + describe("removeFile", () => { + it("should remove a file and return true", () => { + vfs.addFile("test.trb", "content"); + expect(vfs.removeFile("test.trb")).toBe(true); + expect(vfs.hasFile("test.trb")).toBe(false); + }); + + it("should return false for non-existing file", () => { + expect(vfs.removeFile("missing.trb")).toBe(false); + }); + }); + + describe("clear", () => { + it("should remove all files", () => { + vfs.addFiles([ + { path: "a.trb", content: "a" }, + { path: "b.trb", content: "b" }, + ]); + vfs.clear(); + expect(vfs.size).toBe(0); + }); + }); + + describe("getAllFiles", () => { + it("should return a copy of all files", () => { + vfs.addFile("test.trb", "content"); + const files = vfs.getAllFiles(); + + files.set("new.trb", "new"); + expect(vfs.hasFile("new.trb")).toBe(false); + }); + }); +}); diff --git a/tests/escapeRubyString.test.ts b/tests/escapeRubyString.test.ts new file mode 100644 index 0000000..960f5e9 --- /dev/null +++ b/tests/escapeRubyString.test.ts @@ -0,0 +1,38 @@ +import { describe, it, expect } from "vitest"; +import { escapeRubyString } from "../src/utils/escapeRubyString.js"; + +describe("escapeRubyString", () => { + it("should wrap string in double quotes", () => { + expect(escapeRubyString("hello")).toBe('"hello"'); + }); + + it("should escape backslashes", () => { + expect(escapeRubyString("a\\b")).toBe('"a\\\\b"'); + }); + + it("should escape double quotes", () => { + expect(escapeRubyString('say "hi"')).toBe('"say \\"hi\\""'); + }); + + it("should escape newlines", () => { + expect(escapeRubyString("line1\nline2")).toBe('"line1\\nline2"'); + }); + + it("should escape carriage returns", () => { + expect(escapeRubyString("line1\rline2")).toBe('"line1\\rline2"'); + }); + + it("should escape tabs", () => { + expect(escapeRubyString("col1\tcol2")).toBe('"col1\\tcol2"'); + }); + + it("should handle multiple escape sequences", () => { + const input = 'Hello\n"World"\t\\End'; + const expected = '"Hello\\n\\"World\\"\\t\\\\End"'; + expect(escapeRubyString(input)).toBe(expected); + }); + + it("should handle empty string", () => { + expect(escapeRubyString("")).toBe('""'); + }); +}); diff --git a/tests/t-ruby.test.ts b/tests/t-ruby.test.ts new file mode 100644 index 0000000..f9962a2 --- /dev/null +++ b/tests/t-ruby.test.ts @@ -0,0 +1,88 @@ +import { describe, it, expect } from "vitest"; +import { TRuby } from "../src/TRuby.js"; + +describe("TRuby", () => { + describe("constructor", () => { + it("should create an instance with default options", () => { + const tRuby = new TRuby(); + expect(tRuby).toBeInstanceOf(TRuby); + expect(tRuby.isInitialized()).toBe(false); + }); + + it("should create an instance with custom options", () => { + const tRuby = new TRuby({ debug: true }); + expect(tRuby).toBeInstanceOf(TRuby); + }); + }); + + describe("virtual file system", () => { + it("should add and retrieve files", () => { + const tRuby = new TRuby(); + tRuby.addFile("test.trb", "def foo: Integer\n 42\nend"); + + const files = tRuby.getFiles(); + expect(files.size).toBe(1); + expect(files.get("test.trb")).toBe("def foo: Integer\n 42\nend"); + }); + + it("should add multiple files at once", () => { + const tRuby = new TRuby(); + tRuby.addFiles([ + { path: "a.trb", content: "# file a" }, + { path: "b.trb", content: "# file b" }, + ]); + + expect(tRuby.getFiles().size).toBe(2); + }); + + it("should remove files", () => { + const tRuby = new TRuby(); + tRuby.addFile("test.trb", "content"); + expect(tRuby.getFiles().size).toBe(1); + + tRuby.removeFile("test.trb"); + expect(tRuby.getFiles().size).toBe(0); + }); + + it("should clear all files", () => { + const tRuby = new TRuby(); + tRuby.addFiles([ + { path: "a.trb", content: "a" }, + { path: "b.trb", content: "b" }, + ]); + + tRuby.clearFiles(); + expect(tRuby.getFiles().size).toBe(0); + }); + }); + + describe("uninitialized state", () => { + it("should throw when compile called before initialize", async () => { + const tRuby = new TRuby(); + await expect(tRuby.compile("def foo: Integer\n 42\nend")).rejects.toThrow( + "T-Ruby WASM not initialized" + ); + }); + + it("should throw when typeCheck called before initialize", async () => { + const tRuby = new TRuby(); + await expect(tRuby.typeCheck("def foo: Integer\n 42\nend")).rejects.toThrow( + "T-Ruby WASM not initialized" + ); + }); + + it("should throw when eval called before initialize", async () => { + const tRuby = new TRuby(); + await expect(tRuby.eval("1 + 1")).rejects.toThrow( + "T-Ruby WASM not initialized" + ); + }); + + it("should throw when getVersion called before initialize", async () => { + const tRuby = new TRuby(); + await expect(tRuby.getVersion()).rejects.toThrow( + "T-Ruby WASM not initialized" + ); + }); + }); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..564f1d3 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2022", "DOM"], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "dist", + "rootDir": "src", + "resolveJsonModule": true, + "isolatedModules": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/tsup.config.ts b/tsup.config.ts new file mode 100644 index 0000000..1126ca8 --- /dev/null +++ b/tsup.config.ts @@ -0,0 +1,16 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/index.ts"], + format: ["cjs", "esm"], + dts: true, + splitting: false, + sourcemap: true, + clean: true, + treeshake: true, + minify: false, + external: [ + "@ruby/3.4-wasm-wasi", + "@ruby/wasm-wasi", + ], +}); diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..40e5bb0 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + globals: true, + environment: "node", + include: ["src/**/*.test.ts", "tests/**/*.test.ts"], + coverage: { + provider: "v8", + reporter: ["text", "json", "html"], + include: ["src/**/*.ts"], + exclude: ["src/**/*.test.ts", "src/**/*.d.ts"], + }, + }, +});