Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
14 changes: 14 additions & 0 deletions .changeset/rust-translation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
'command-stream': minor
---

Add Rust translation and reorganize codebase

- Reorganize JavaScript source files into `js/` folder structure
- Move tests from root `tests/` to `js/tests/`
- Add complete Rust translation in `rust/` folder with:
- Shell parser supporting &&, ||, ;, |, (), and redirections
- All 21 virtual commands (cat, cp, mv, rm, touch, mkdir, ls, cd, pwd, echo, yes, seq, sleep, env, which, test, exit, basename, dirname, true, false)
- ProcessRunner for async command execution with tokio
- Comprehensive test suite mirroring JavaScript tests
- Case study documentation in docs/case-studies/issue-146/
4 changes: 2 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ jobs:

- name: Run tests (Bun)
if: matrix.runtime == 'bun'
run: bun test tests/
run: bun test js/tests/
env:
COMMAND_STREAM_VERBOSE: true

Expand All @@ -166,7 +166,7 @@ jobs:
run: |
node -e "
try {
const { $ } = require('./src/\$.mjs');
const { $ } = require('./js/src/\$.mjs');
console.log('✅ Module loads successfully in Node.js ${{ matrix.node-version }}');
} catch (error) {
console.error('❌ Module failed to load:', error.message);
Expand Down
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -142,4 +142,7 @@ vite.config.ts.timestamp-*
.persisted-configs

# jscpd reports
reports/
reports/

# Rust build artifacts
rust/target/
4 changes: 2 additions & 2 deletions bunfig.toml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Bun configuration file

[test]
# Only run tests from the tests/ directory
root = "./tests"
# Only run tests from the js/tests/ directory
root = "./js/tests"

# Increase timeout from default 5000ms to 10000ms (2x) for CI stability
timeout = 10000
Expand Down
2 changes: 1 addition & 1 deletion claude-profiles.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* - Verbose logging and file logging support
*/

import { $ } from './src/$.mjs';
import { $ } from './js/src/$.mjs';
import fs, { createWriteStream, promises as fsPromises } from 'fs';
import path from 'path';
import os from 'os';
Expand Down
244 changes: 244 additions & 0 deletions docs/case-studies/issue-146/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
# Case Study: JavaScript to Rust Translation (Issue #146)

## Summary

This document provides a comprehensive analysis of the process, challenges, and lessons learned from translating the command-stream JavaScript library to Rust.

## Project Overview

### Original JavaScript Codebase

- **Main file**: `src/$.mjs` (~6,765 lines)
- **Shell parser**: `src/shell-parser.mjs` (~403 lines)
- **Utilities**: `src/$.utils.mjs` (~101 lines)
- **Virtual commands**: 21 command files in `src/commands/`
- **Total**: ~8,400 lines of JavaScript

### Rust Translation

- **Main library**: `rust/src/lib.rs`
- **Shell parser**: `rust/src/shell_parser.rs`
- **Utilities**: `rust/src/utils.rs`
- **Virtual commands**: 21 command modules in `rust/src/commands/`

## Timeline of Development

### Phase 1: Code Organization

1. Created `js/` folder structure to house JavaScript code
2. Updated `package.json` to point to new `js/src/` location
3. Updated all import statements in tests and examples

### Phase 2: Rust Project Setup

1. Created `rust/` folder with Cargo.toml
2. Defined dependencies:
- `tokio` for async runtime
- `which` for command lookup
- `nix` for Unix signal handling
- `regex` for pattern matching
- `chrono` for timestamps
- `filetime` for file timestamp operations

### Phase 3: Core Translation

1. Translated shell parser (tokenizer, parser, AST types)
2. Translated utilities (tracing, command results, ANSI handling)
3. Translated main library (ProcessRunner, shell detection)
4. Translated all 21 virtual commands

## Key Translation Patterns

### 1. JavaScript Async to Rust Async

**JavaScript:**

```javascript
async function sleep({ args, abortSignal }) {
const seconds = parseFloat(args[0] || 0);
await new Promise((resolve) => setTimeout(resolve, seconds * 1000));
return { stdout: '', code: 0 };
}
```

**Rust:**

```rust
pub async fn sleep(ctx: CommandContext) -> CommandResult {
let seconds: f64 = ctx.args.first()
.and_then(|s| s.parse().ok())
.unwrap_or(0.0);

tokio::time::sleep(Duration::from_secs_f64(seconds)).await;
CommandResult::success_empty()
}
```

### 2. JavaScript Object Literals to Rust Structs

**JavaScript:**

```javascript
const result = {
stdout: output,
stderr: '',
code: 0,
async text() {
return this.stdout;
},
};
```

**Rust:**

```rust
#[derive(Debug, Clone)]
pub struct CommandResult {
pub stdout: String,
pub stderr: String,
pub code: i32,
}

impl CommandResult {
pub fn success(stdout: impl Into<String>) -> Self {
CommandResult {
stdout: stdout.into(),
stderr: String::new(),
code: 0,
}
}
}
```

### 3. JavaScript Closures to Rust Trait Objects

**JavaScript:**

```javascript
function trace(category, messageOrFunc) {
const message =
typeof messageOrFunc === 'function' ? messageOrFunc() : messageOrFunc;
console.error(`[TRACE] [${category}] ${message}`);
}
```

**Rust:**

```rust
pub fn trace_lazy<F>(category: &str, message_fn: F)
where
F: FnOnce() -> String,
{
if !is_trace_enabled() {
return;
}
trace(category, &message_fn());
}
```

### 4. JavaScript Error Handling to Rust Result Types

**JavaScript:**

```javascript
try {
const content = fs.readFileSync(path, 'utf8');
return { stdout: content, code: 0 };
} catch (error) {
if (error.code === 'ENOENT') {
return { stderr: `cat: ${file}: No such file or directory`, code: 1 };
}
throw error;
}
```

**Rust:**

```rust
match fs::read_to_string(&path) {
Ok(content) => CommandResult::success(content),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
CommandResult::error(format!("cat: {}: No such file or directory\n", file))
}
Err(e) => CommandResult::error(format!("cat: {}: {}\n", file, e)),
}
```

## Challenges Encountered

### 1. Tagged Template Literals

JavaScript's tagged template literal syntax `$\`echo hello\``has no direct Rust equivalent. We implemented the`$()` function as a regular function call instead.

### 2. Event Emitter Pattern

JavaScript's EventEmitter pattern required translation to Rust's channel-based communication using `tokio::sync::mpsc`.

### 3. Process Group Handling

Unix process group management differs between Node.js and Rust. We used the `nix` crate for proper signal handling.

### 4. Async Iterator Pattern

JavaScript's `for await (const chunk of stream)` was translated to Rust's async stream patterns using channels.

## Lessons Learned

### 1. Type Safety Benefits

Rust's type system caught several edge cases that existed in the JavaScript code:

- Null/undefined handling became explicit with `Option<T>`
- Error handling became explicit with `Result<T, E>`
- String encoding issues were caught at compile time

### 2. Memory Management

Rust's ownership model required explicit decisions about:

- When to clone vs borrow data
- Lifetime of process handles
- Cleanup of resources on cancellation

### 3. Cross-Platform Considerations

Both JavaScript and Rust require platform-specific code for:

- Shell detection (Windows vs Unix)
- Signal handling (SIGINT, SIGTERM)
- File permissions

### 4. Testing Strategy

Unit tests were essential for:

- Verifying parity with JavaScript behavior
- Catching edge cases early
- Documenting expected behavior

## Architecture Comparison

| Component | JavaScript | Rust |
| --------------- | ---------------------- | ----------------------- |
| Async Runtime | Node.js/Bun event loop | Tokio |
| Process Spawn | child_process.spawn | tokio::process::Command |
| Channels | EventEmitter | mpsc channels |
| Error Handling | try/catch | Result<T, E> |
| String Handling | UTF-16 strings | UTF-8 String |
| File I/O | fs module | std::fs |
| Signal Handling | process.on('SIGINT') | tokio::signal |

## Future Improvements

1. **Streaming Improvements**: Implement async iterator traits for better streaming support
2. **Error Types**: Create more specific error types for different failure modes
3. **Performance**: Benchmark and optimize critical paths
4. **Platform Support**: Add more Windows-specific implementations
5. **CI/CD**: Add Rust builds to existing CI pipeline

## References

- Original Issue: https://github.com/link-foundation/command-stream/issues/146
- Pull Request: https://github.com/link-foundation/command-stream/pull/147
- Rust Book: https://doc.rust-lang.org/book/
- Tokio Documentation: https://tokio.rs/
19 changes: 16 additions & 3 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,14 @@ export default [
},
{
// Test files have different requirements
files: ['tests/**/*.js', 'tests/**/*.mjs', '**/*.test.js', '**/*.test.mjs'],
files: [
'tests/**/*.js',
'tests/**/*.mjs',
'js/tests/**/*.js',
'js/tests/**/*.mjs',
'**/*.test.js',
'**/*.test.mjs',
],
rules: {
'no-unused-vars': 'off', // Tests often have unused vars for demonstration or intentional non-use
'require-await': 'off', // Async functions without await are common in tests
Expand All @@ -138,7 +145,13 @@ export default [
},
{
// Example and debug files are more lenient
files: ['examples/**/*.js', 'examples/**/*.mjs', 'claude-profiles.mjs'],
files: [
'examples/**/*.js',
'examples/**/*.mjs',
'js/examples/**/*.js',
'js/examples/**/*.mjs',
'claude-profiles.mjs',
],
rules: {
'no-unused-vars': 'off', // Examples often have unused vars for demonstration
'require-await': 'off', // Async functions without await are common in examples
Expand All @@ -160,7 +173,7 @@ export default [
},
{
// Virtual command implementations have specific interface requirements
files: ['src/commands/**/*.mjs'],
files: ['src/commands/**/*.mjs', 'js/src/commands/**/*.mjs'],
rules: {
'require-await': 'off', // Commands must be async to match interface even if they don't await
complexity: 'off', // Commands can be complex due to argument parsing and validation
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Loading
Loading