# How Roo Code's Custom Tools System Uses esbuild for Dynamic Tool Execution

> Discover how Roo Code leverages esbuild for dynamic tool execution, transpiling TypeScript on-demand, bundling dependencies, and caching output as executable modules.

- Repository: [Roo Code/Roo-Code](https://github.com/RooCodeInc/Roo-Code)
- Tags: internals
- Published: 2026-04-26

---

**Roo Code's custom tools system uses esbuild-wasm as a cross-platform CLI process to transpile TypeScript files on-demand, bundling dependencies while externalizing Node.js built-ins, then caches the output as executable ES modules.**

The Roo-Code repository provides a plug-in-like framework that allows developers to drop arbitrary TypeScript or JavaScript files into a directory and execute them at runtime within the VS Code extension. This custom tools system leverages esbuild-wasm to handle just-in-time compilation, isolating the build process from the host extension while maintaining compatibility across macOS, Windows, and Linux environments.

## Architecture Overview

The system centers on the `CustomToolRegistry` class defined in [`packages/core/src/custom-tools/custom-tool-registry.ts`](https://github.com/RooCodeInc/Roo-Code/blob/main/packages/core/src/custom-tools/custom-tool-registry.ts). During extension activation in [`src/extension.ts`](https://github.com/RooCodeInc/Roo-Code/blob/main/src/extension.ts), the registry's `setExtensionPath()` method stores the extension root path, enabling the system to locate the esbuild binary in both production and development contexts. The modular architecture separates binary resolution ([`esbuild-runner.ts`](https://github.com/RooCodeInc/Roo-Code/blob/main/esbuild-runner.ts)) from tool orchestration, ensuring clean separation between the compilation layer and execution environment.

## The Three-Stage Execution Flow

### Stage 1: Resolving the esbuild Binary

In [`packages/core/src/custom-tools/esbuild-runner.ts`](https://github.com/RooCodeInc/Roo-Code/blob/main/packages/core/src/custom-tools/esbuild-runner.ts), the `getEsbuildScriptPath()` function locates the appropriate esbuild executable. The implementation checks for a bundled `dist/bin/esbuild` path first (used in production builds), falling back to `node_modules/esbuild-wasm/bin/esbuild` during development. This resolution strategy ensures the WASM-based compiler is available regardless of the host platform or sandbox restrictions.

The binary path is determined at runtime through logic in lines 19-27 of [`esbuild-runner.ts`](https://github.com/RooCodeInc/Roo-Code/blob/main/esbuild-runner.ts), allowing the same codebase to function both in packaged extensions and local development environments.

### Stage 2: On-the-Fly Transpilation and Bundling

When `CustomToolRegistry.import()` detects a `.ts` file, it first validates the tool definition (name, description, and optional Zod schema) then checks an in-memory cache. If the tool is uncached, the registry invokes `runEsbuild()` with a tightly controlled configuration:

- **Entry point bundling** combines the tool code and npm dependencies into a single file
- **External Node built-ins** prevent bundling of core modules using `external: NODE_BUILTIN_MODULES`
- **CommonJS compatibility** is achieved via `banner: COMMONJS_REQUIRE_BANNER`, which injects a require shim
- **Dependency resolution** uses `nodePaths` to include the tool's local `node_modules` directory

The CLI process, spawned via `execa(process.execPath, args)`, writes a `bundle.mjs` file to a tool-specific cache directory. This output is an ES module containing the transpiled code and dependency shims, ready for immediate execution.

### Stage 3: Dynamic Execution

The generated `bundle.mjs` is loaded using dynamic `import()` with a `file://` URL. Because the bundle includes the CommonJS shim, tools can freely use `require()` for dependencies despite running in an ESM context. The registry stores loaded modules in `this.tsCache`, a Map that persists for the duration of the extension session, while the on-disk cache ensures fast reloads across extension restarts.

Subsequent invocations of the same tool skip the esbuild process entirely, reading directly from the in-memory cache or the cached `bundle.mjs` file.

## Why esbuild-wasm Uses a CLI Approach

The decision to invoke esbuild as a subprocess rather than using the JavaScript API provides three architectural advantages:

- **Cross-platform guarantee**: The WASM implementation works uniformly on macOS, Windows, Linux, and within VS Code's extension host without platform-specific native binaries
- **Process isolation**: Running as a separate Node.js process prevents build-time state or memory leaks from affecting the host extension
- **Deterministic caching**: The CLI writes output to predictable filesystem locations, enabling reliable cache reuse and simplifying cache invalidation logic

## Implementation Examples

### TypeScript Custom Tool Definition

```typescript
// tools/helloWorld.ts
import { z } from "zod"

export const helloWorld = {
  name: "helloWorld",
  description: "Returns a friendly greeting",
  parameters: z.object({ person: z.string() }),
  async execute({ person }: { person: string }) {
    return `Hello, ${person}! 👋`
  },
}

```

### Loading and Executing Tools at Runtime

```typescript
import { customToolRegistry } from "@roo-code/core/custom-tools"

// Called during extension activation in src/extension.ts
customToolRegistry.setExtensionPath(context.extensionPath)

// Load all tools from an absolute directory path
await customToolRegistry.loadFromDirectory("/absolute/path/to/tools")

// Retrieve the bundled tool and execute
const tool = customToolRegistry.get("helloWorld")
if (tool) {
  const result = await tool.execute({ person: "Alice" })
  console.log(result) // → Hello, Alice! 👋
}

```

### Build-Time Binary Packaging

The build process in [`packages/build/src/esbuild.ts`](https://github.com/RooCodeInc/Roo-Code/blob/main/packages/build/src/esbuild.ts) ensures the esbuild-wasm binary is available in production by copying it to the distribution directory:

```typescript
// From packages/build/src/esbuild.ts (lines 162-195)
await fs.copyFile(
  require.resolve("esbuild-wasm/bin/esbuild"),
  join(outputDir, "bin", "esbuild")
)
// Also copies the associated .wasm file

```

## Summary

- Roo Code's custom tools system uses **esbuild-wasm** invoked as a CLI process for cross-platform TypeScript compilation
- The `CustomToolRegistry` class orchestrates tool discovery through `loadFromDirectory()` and manages execution via the `import()` method
- Tools are bundled with npm dependencies while Node.js built-ins remain external, producing a cached `bundle.mjs` output
- A two-tier caching system (in-memory `this.tsCache` and filesystem storage) eliminates redundant compilation overhead
- The CommonJS shim injected via the `banner` option enables tools to use `require()` despite the ESM-based execution environment

## Frequently Asked Questions

### Why does Roo Code use esbuild-wasm instead of the native esbuild binary?

Roo Code uses esbuild-wasm to ensure the custom tools system works across all platforms—including VS Code's sandboxed extension host—without requiring platform-specific native binaries. The WASM implementation provides identical bundling performance while maintaining compatibility with macOS, Windows, and Linux environments without additional configuration.

### How does the caching mechanism work for custom tools?

The system implements a two-tier cache: an in-memory Map (`this.tsCache`) stores loaded modules during the current extension session, while a filesystem cache preserves bundled `bundle.mjs` files in tool-specific subdirectories. When `CustomToolRegistry.import()` encounters a previously compiled tool, it bypasses the esbuild invocation entirely and loads the cached version directly from memory or disk.

### Can custom tools use CommonJS require statements?

Yes. The bundling process in [`esbuild-runner.ts`](https://github.com/RooCodeInc/Roo-Code/blob/main/esbuild-runner.ts) injects a CommonJS require shim via the `banner: COMMONJS_REQUIRE_BANNER` option, allowing tools to use `require()` for dependencies. The output `bundle.mjs` is an ES module that includes this shim, enabling seamless interoperability between CommonJS npm packages and the ESM-based execution environment of the extension.

### Where is the esbuild binary located in production builds?

In production builds, the esbuild binary is bundled at `dist/bin/esbuild` within the extension package, copied there by the build script in [`packages/build/src/esbuild.ts`](https://github.com/RooCodeInc/Roo-Code/blob/main/packages/build/src/esbuild.ts). The `getEsbuildScriptPath()` function checks this location first, only falling back to the development `node_modules` path when the bundled binary is unavailable.