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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/node-compat.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ on:
- 'src/virtual-fs.ts'
- '.github/workflows/node-compat.yml'

permissions:
contents: read
pull-requests: write

jobs:
test:
name: Node.js Compatibility
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,7 @@ npm-debug.log*

# Temp folders for testing
temp/

# Scratch files
e2e/deploy-debug.spec.ts
examples/macaly-demo.html
27 changes: 27 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,33 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.2.12] - 2026-02-12

### Added

- **Generic bin stubs:** `npm install` now reads each package's `bin` field and creates executable scripts in `/node_modules/.bin/`. CLI tools like `vitest`, `eslint`, `tsc`, etc. work automatically via the `node` command — no custom commands needed.
- **Streaming `container.run()` API:** Long-running commands support `onStdout`/`onStderr` callbacks and `AbortController` signal for cancellation.
- **`container.sendInput()`:** Send stdin data to running processes (emits both `data` and `keypress` events for readline compatibility).
- **Vitest demo with xterm.js:** New `examples/vitest-demo.html` showcasing real vitest execution in the browser with watch mode, syntax-highlighted terminal output, and file editing.
- **E2E tests for vitest demo:** 5 Playwright tests covering install, test execution, tab switching, failure detection, and watch mode restart.
- **`rollup` shim:** Stub module so vitest's dependency chain resolves without errors.
- **`fs.realpathSync.native`:** Added as alias for `realpathSync` (used by vitest internals).
- **`fs.createReadStream` / `fs.createWriteStream`:** Basic implementations using VirtualFS.
- **`path.delimiter` and `path.win32`:** Added missing path module properties.
- **`process.getuid()`, `process.getgid()`, `process.umask()`:** Added missing process methods used by npm packages.
- **`util.deprecate()`:** Returns the original function with a no-op deprecation warning.

### Changed

- **`Object.defineProperty` patch on `globalThis`:** Forces `configurable: true` for properties defined on `globalThis`, so libraries that define non-configurable globals (like vitest's `__vitest_index__`) can be re-run without errors.
- **VFS adapter executable mode:** Files in `/node_modules/.bin/` now return `0o755` mode so just-bash treats them as executable.
- **`Runtime.clearCache()` clears in-place:** Previously created a new empty object, leaving closures referencing the stale cache. Now deletes keys in-place.
- **Watch mode uses restart pattern:** Vitest caches modules internally (Vite's ModuleRunner), so file changes require a full vitest restart (abort + re-launch) rather than stdin-triggered re-runs.

### Removed

- **Custom vitest command:** Deleted `src/shims/vitest-command.ts` and removed vitest-specific handling from `child_process.ts`. Vitest now runs through the generic bin stub + `node` command like any other CLI tool.

## [0.2.11] - 2026-02-09

### Fixed
Expand Down
90 changes: 89 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ Built by the creators of [Macaly.com](https://macaly.com) — a tool that lets a

- **Virtual File System** - Full in-memory filesystem with Node.js-compatible API
- **Node.js API Shims** - 40+ shimmed modules (`fs`, `path`, `http`, `events`, and more)
- **npm Package Installation** - Install and run real npm packages in the browser
- **npm Package Installation** - Install and run real npm packages in the browser with automatic bin stub creation
- **Run Any CLI Tool** - npm packages with `bin` entries (vitest, eslint, tsc, etc.) work automatically
- **Dev Servers** - Built-in Vite and Next.js development servers
- **Hot Module Replacement** - React Refresh support for instant updates
- **TypeScript Support** - First-class TypeScript/TSX transformation via esbuild-wasm
Expand Down Expand Up @@ -127,6 +128,69 @@ container.execute(`
// Output: Hello world
```

### Running Shell Commands

```typescript
import { createContainer } from 'almostnode';

const container = createContainer();

// Write a package.json with scripts
container.vfs.writeFileSync('/package.json', JSON.stringify({
name: 'my-app',
scripts: {
build: 'echo Building...',
test: 'vitest run'
}
}));

// Run shell commands directly
const result = await container.run('npm run build');
console.log(result.stdout); // "Building..."

await container.run('npm test');
await container.run('echo hello && echo world');
await container.run('ls /');
```

Supported npm commands: `npm run <script>`, `npm start`, `npm test`, `npm install`, `npm ls`.
Pre/post lifecycle scripts (`prebuild`, `postbuild`, etc.) run automatically.

### Running CLI Tools

Any npm package with a `bin` field works automatically after install — no configuration needed.

```typescript
// Install a package that includes a CLI tool
await container.npm.install('vitest');

// Run it directly — bin stubs are created in /node_modules/.bin/
const result = await container.run('vitest run');
console.log(result.stdout); // Test results
```

This works because `npm install` reads each package's `bin` field and creates executable scripts in `/node_modules/.bin/`. The shell's PATH includes `/node_modules/.bin`, so tools like `vitest`, `eslint`, `tsc`, etc. resolve automatically.

### Streaming Output & Long-Running Commands

For commands that run continuously (like watch mode), use streaming callbacks and abort signals:

```typescript
const controller = new AbortController();

await container.run('vitest --watch', {
onStdout: (data) => console.log(data),
onStderr: (data) => console.error(data),
signal: controller.signal,
});

// Send input to the running process
container.sendInput('a'); // Press 'a' to re-run all tests

// Stop the command
controller.abort();
```

### With Next.js Dev Server

```typescript
Expand Down Expand Up @@ -395,6 +459,30 @@ Returns:
- `container.runtime` - Runtime instance
- `container.npm` - PackageManager instance
- `container.serverBridge` - ServerBridge instance
- `container.run(command, options?)` - Run a shell command (returns `Promise<RunResult>`)
- `container.sendInput(data)` - Send stdin data to the currently running process
- `container.execute(code)` - Execute JavaScript code
- `container.runFile(filename)` - Run a file from VirtualFS

#### `container.run(command, options?)`

```typescript
interface RunResult {
stdout: string;
stderr: string;
exitCode: number;
}

interface RunOptions {
onStdout?: (data: string) => void; // Stream stdout in real-time
onStderr?: (data: string) => void; // Stream stderr in real-time
signal?: AbortSignal; // Cancel the command
}
```

#### `container.sendInput(data)`

Sends data to the stdin of the currently running process. Emits both `data` and `keypress` events for compatibility with readline-based tools (e.g., vitest watch mode).

### VirtualFS

Expand Down
57 changes: 56 additions & 1 deletion docs/core-concepts.html
Original file line number Diff line number Diff line change
Expand Up @@ -239,12 +239,67 @@ <h3>Available Commands</h3>
<li><strong>Utilities</strong> — <code>echo</code>, <code>env</code>, <code>find</code>, <code>pwd</code>, <code>date</code>, <code>seq</code>, <code>xargs</code>, <code>tee</code>, <code>which</code></li>
<li><strong>Shell features</strong> — pipes (<code>|</code>), redirects (<code>&gt;</code>, <code>&gt;&gt;</code>), environment variables, subshells</li>
<li><strong>Custom: <code>node &lt;script&gt;</code></strong> — Runs a JavaScript file using the almostnode Runtime</li>
<li><strong>Custom: <code>npm &lt;command&gt;</code></strong> — Runs npm scripts, installs packages, and lists dependencies (see below)</li>
<li><strong>Custom: <code>vitest run</code></strong> — Runs vitest unit tests once using real <code>@vitest/expect</code> assertions (after <code>npm install vitest</code>)</li>
<li><strong>Custom: <code>vitest</code> / <code>vitest watch</code></strong> — Watch mode: runs tests then re-runs automatically when VFS files change. Use with <code>container.run()</code> streaming options (see below)</li>
<li><strong>Custom: <code>convex &lt;args&gt;</code></strong> — Runs the Convex CLI bundle (after <code>npm install convex</code>)</li>
</ul>

<h3>npm Scripts</h3>
<p>Run scripts defined in <code>package.json</code> using the built-in <code>npm</code> command. Supports <code>npm run &lt;script&gt;</code>, <code>npm start</code>, <code>npm test</code>, <code>npm install</code>, and <code>npm ls</code>. Pre/post lifecycle scripts (<code>prebuild</code>, <code>postbuild</code>, etc.) run automatically.</p>

<pre><code><span class="kw">const</span> container <span class="op">=</span> <span class="fn">createContainer</span><span class="op">();</span>

<span class="cm">// Run shell commands directly with container.run()</span>
<span class="kw">const</span> result <span class="op">=</span> <span class="kw">await</span> container<span class="op">.</span><span class="fn">run</span><span class="op">(</span><span class="str">'npm run build'</span><span class="op">);</span>
console<span class="op">.</span><span class="fn">log</span><span class="op">(</span>result<span class="op">.</span>stdout<span class="op">);</span>

<span class="kw">await</span> container<span class="op">.</span><span class="fn">run</span><span class="op">(</span><span class="str">'npm test'</span><span class="op">);</span>
<span class="kw">await</span> container<span class="op">.</span><span class="fn">run</span><span class="op">(</span><span class="str">'ls /'</span><span class="op">);</span></code></pre>

<p>Try the <a href="../examples/npm-scripts-demo.html">interactive npm scripts demo</a> or the <a href="../examples/vitest-demo.html">vitest testing demo</a> to see it in action.</p>

<h3>Streaming Output &amp; Watch Mode</h3>
<p><code>container.run()</code> accepts optional streaming callbacks and an <code>AbortSignal</code> for long-running commands like vitest watch mode:</p>

<pre><code><span class="kw">const</span> controller <span class="op">=</span> <span class="kw">new</span> <span class="fn">AbortController</span><span class="op">();</span>

<span class="cm">// Start vitest in watch mode with streaming output</span>
container<span class="op">.</span><span class="fn">run</span><span class="op">(</span><span class="str">'vitest'</span><span class="op">,</span> <span class="op">{</span>
<span class="fn">onStdout</span><span class="op">:</span> <span class="op">(</span>data<span class="op">)</span> <span class="op">=&gt;</span> console<span class="op">.</span><span class="fn">log</span><span class="op">(</span>data<span class="op">),</span>
<span class="fn">onStderr</span><span class="op">:</span> <span class="op">(</span>data<span class="op">)</span> <span class="op">=&gt;</span> console<span class="op">.</span><span class="fn">error</span><span class="op">(</span>data<span class="op">),</span>
signal<span class="op">:</span> controller<span class="op">.</span>signal<span class="op">,</span>
<span class="op">});</span>

<span class="cm">// Tests re-run automatically when VFS files change.</span>
<span class="cm">// Stop watching:</span>
controller<span class="op">.</span><span class="fn">abort</span><span class="op">();</span></code></pre>

<h3>Installing Packages — API Overview</h3>
<p>There are multiple ways to install npm packages, each suited for different use cases:</p>

<table>
<tr>
<th>Method</th>
<th>Best for</th>
</tr>
<tr>
<td><code>container.npm.install('pkg')</code></td>
<td>Programmatic use — typed <code>InstallResult</code>, <code>onProgress</code> callback, <code>save</code>/<code>saveDev</code> options</td>
</tr>
<tr>
<td><code>container.npm.installFromPackageJson()</code></td>
<td>Install all dependencies from an existing <code>package.json</code></td>
</tr>
<tr>
<td><code>container.run('npm install pkg')</code></td>
<td>Shell workflows, interactive terminals, scripts</td>
</tr>
</table>

<div class="callout">
<div class="callout-title">Note</div>
<p><code>execSync()</code> is not supported in the browser — use the async <code>exec()</code> with callbacks or promises instead. The shell operates on the VirtualFS, not the real filesystem.</p>
<p><code>execSync()</code> is not supported in the browser — use the async <code>exec()</code> with callbacks, or the <code>container.run()</code> convenience method which returns a Promise. The shell operates on the VirtualFS, not the real filesystem.</p>
</div>

<h2>Convex Integration</h2>
Expand Down
Loading