Skip to main content
Build WebAssembly plugins using TypeScript. Noorle uses ComponentizeJS to compile TypeScript to WASI Preview 2 WebAssembly.

Setup

Install Tools

# Install Noorle CLI
curl -L cli.noorle.dev | sh

# Create a new TypeScript plugin
noorle plugin new my-ts-plugin --lang typescript
cd my-ts-plugin

Project Structure

my-ts-plugin/
├── src/
│   ├── index.ts              # Entry point
│   ├── tools.ts              # Tool implementations
│   └── utils.ts              # Utilities
├── tests/
│   └── tools.test.ts
├── package.json              # Dependencies
├── tsconfig.json             # TypeScript config
├── noorle.yaml               # Plugin config
├── world.wit                 # Component interface
├── .env
└── README.md

Define Tools

Export functions as plugin tools.

Simple Functions

// src/tools.ts
export function greet(name: string): string {
  return `Hello, ${name}!`;
}

export function add(a: number, b: number): number {
  return a + b;
}

export function multiply(a: number, b: number): number {
  return a * b;
}

Complex Types

interface Item {
  id: string;
  name: string;
  count: number;
}

interface Config {
  [key: string]: string;
}

export function processItems(
  items: Item[],
  config: Config
): Item[] {
  return items.map(item => ({
    ...item,
    name: `${config.prefix}${item.name}`
  }));
}

export function searchItems(
  query: string,
  limit: number = 10
): Item[] {
  return Array.from({ length: Math.min(limit, 5) }, (_, i) => ({
    id: `${i}`,
    name: `${query}_result_${i}`,
    count: i + 1
  }));
}

Type Definitions

Define reusable types:
// src/types.ts
export interface User {
  id: string;
  name: string;
  email: string;
  active: boolean;
}

export interface Result<T> {
  success: boolean;
  data?: T;
  error?: string;
}

export interface ApiResponse {
  status: number;
  body: string;
  headers: Record<string, string>;
}

// Use in tools
export function validateUser(user: User): Result<User> {
  if (!user.email.includes("@")) {
    return { success: false, error: "Invalid email" };
  }
  return { success: true, data: user };
}

Working with Dependencies

Add Dependencies

Edit package.json:
{
  "name": "my-ts-plugin",
  "version": "0.1.0",
  "dependencies": {
    "axios": "^1.6.0",
    "lodash-es": "^4.17.21",
    "uuid": "^9.0.0"
  },
  "devDependencies": {
    "typescript": "^5.0.0",
    "@types/node": "^20.0.0"
  }
}

Install and Build

# Install dependencies
npm install

# Build the plugin
noorle plugin build

Size Optimization

Choose lightweight dependencies:
{
  "dependencies": {
    // ❌ Heavy
    "axios": "^1.6.0",        // ~60KB with dependencies
    "lodash": "^4.17.0",       // ~50KB

    // ✅ Lightweight
    "uuid": "^9.0.0",          // ~10KB
    "date-fns": "^2.30.0"      // ~20KB
  }
}

File I/O

Access filesystem via standard APIs.
import * as fs from "fs";
import * as path from "path";

export function readConfig(): string {
  const configPath = "/allowed/path/config.json";
  return fs.readFileSync(configPath, "utf-8");
}

export function writeCache(key: string, value: string): boolean {
  const cacheDir = process.env.CACHE_DIR || "/cache";
  const filePath = path.join(cacheDir, `${key}.txt`);
  fs.writeFileSync(filePath, value);
  return true;
}

export function listFiles(directory: string): string[] {
  return fs.readdirSync(directory)
    .filter(f => fs.statSync(path.join(directory, f)).isFile())
    .map(f => f);
}

Configure Permissions

In noorle.yaml:
permissions:
  filesystem:
    allow:
      - uri: "fs://cache/**"
        access: [read, write]
      - uri: "fs://config/**"
        access: [read]

  environment:
    allow:
      - key: "CACHE_DIR"

Environment Variables

Access configuration via environment variables.
export function getApiCredentials(): Record<string, string> {
  return {
    apiKey: process.env.API_KEY || "",
    apiSecret: process.env.API_SECRET || "",
  };
}

export function getDatabaseUrl(): string {
  return process.env.DATABASE_URL || "sqlite:///:memory:";
}

export function getConfig(): Record<string, string> {
  return {
    debug: process.env.DEBUG === "true" ? "true" : "false",
    logLevel: process.env.LOG_LEVEL || "info",
    timeout: process.env.TIMEOUT || "30000",
  };
}
Configure in .env:
API_KEY=sk-1234567890
API_SECRET=secret-value
DATABASE_URL=postgres://localhost/mydb
DEBUG=true
LOG_LEVEL=debug

Network Access

Make HTTP requests to allowed hosts.
export async function fetchData(url: string): Promise<string> {
  const response = await fetch(url);
  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`);
  }
  return response.text();
}

export async function postJson(
  url: string,
  data: Record<string, unknown>
): Promise<string> {
  const response = await fetch(url, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(data),
  });
  return response.text();
}

export async function callApi(
  endpoint: string,
  params: Record<string, string>
): Promise<Record<string, unknown>> {
  const url = new URL(`https://api.example.com/${endpoint}`);
  Object.entries(params).forEach(([key, value]) => {
    url.searchParams.append(key, value);
  });

  const response = await fetch(url.toString());
  return response.json();
}
Or use a library like axios (add to package.json):
import axios from "axios";

export async function callApiAxios(endpoint: string): Promise<string> {
  const response = await axios.get(
    `https://api.example.com/${endpoint}`,
    { timeout: 5000 }
  );
  return response.data;
}
Configure network permissions:
permissions:
  network:
    allow:
      - host: "api.example.com"
      - host: "*.github.com"

Error Handling

Return errors gracefully with typed results:
interface ErrorResult {
  success: false;
  error: string;
}

interface SuccessResult<T> {
  success: true;
  data: T;
}

type Result<T> = SuccessResult<T> | ErrorResult;

export function safeOperation(input: string): Result<string> {
  try {
    if (!input) {
      return { success: false, error: "Empty input" };
    }

    const result = process(input);
    return { success: true, data: result };
  } catch (error) {
    return {
      success: false,
      error: error instanceof Error ? error.message : "Unknown error"
    };
  }
}

function process(input: string): string {
  if (typeof input !== "string") {
    throw new Error("Expected string input");
  }
  return input.toUpperCase();
}

Testing

Test locally before uploading.

Unit Tests

// tests/tools.test.ts
import { greet, add, processItems } from "../src/tools";

describe("Tools", () => {
  test("greet returns greeting", () => {
    expect(greet("Alice")).toBe("Hello, Alice!");
  });

  test("add performs addition", () => {
    expect(add(2, 3)).toBe(5);
    expect(add(1.5, 2.5)).toBe(4.0);
  });

  test("processItems transforms items", () => {
    const items = [{ id: "1", name: "test", count: 5 }];
    const config = { prefix: "new_" };
    const result = processItems(items, config);
    expect(result[0].name).toBe("new_test");
  });
});
Run tests:
npm test

Integration Tests

# Build and test the plugin
noorle plugin build
noorle plugin test my_tool --input '{"param": "value"}'

Building and Deploying

Build

noorle plugin build

# Output:
# ✓ Building plugin...
# ✓ Compiling TypeScript to WebAssembly
# ✓ Creating my-ts-plugin.npack (156KB)

Upload

noorle plugin push --api-key YOUR_KEY

# Output:
# ✓ Uploading plugin...
# ✓ Plugin ID: 550e8400-e29b-41d4-a716-446655440000
# ✓ Active version: v1
# ✓ Tools: greet, add, processItems

Best Practices

Input Validation

export function validateEmail(email: string): Record<string, unknown> {
  const errors: string[] = [];

  if (!email || email.trim().length === 0) {
    errors.push("Email cannot be empty");
  }

  if (!email.includes("@")) {
    errors.push("Email must contain @");
  }

  if (email.length > 255) {
    errors.push("Email too long");
  }

  return errors.length > 0
    ? { valid: false, errors }
    : { valid: true };
}

Logging

function log(message: string, level: "INFO" | "DEBUG" | "ERROR" = "INFO") {
  console.error(`[${level}] ${message}`);
}

export function myTool(input: string): string {
  log(`Processing: ${input}`);
  const result = process(input);
  log(`Completed`, "DEBUG");
  return result;
}

Documentation

/**
 * Transform input text.
 *
 * @param text - The text to transform
 * @param uppercase - Convert to uppercase if true
 * @param trim - Remove whitespace if true
 * @returns Transformed text
 */
export function transformText(
  text: string,
  uppercase: boolean = false,
  trim: boolean = true
): string {
  if (trim) text = text.trim();
  if (uppercase) text = text.toUpperCase();
  return text;
}

Example: Complete Plugin

// src/weather.ts
import * as fs from "fs";

interface WeatherData {
  city: string;
  temp: number;
  description: string;
}

export async function getWeather(
  city: string,
  units: "celsius" | "fahrenheit" = "celsius"
): Promise<Record<string, unknown>> {
  const apiKey = process.env.OPENWEATHER_API_KEY;
  if (!apiKey) {
    return { error: "API key not configured" };
  }

  const url = `https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${apiKey}&units=${units}`;

  try {
    const response = await fetch(url);
    const data = await response.json();

    return {
      city: data.name,
      temp: data.main.temp,
      description: data.weather[0].description,
    };
  } catch (error) {
    return {
      error: `Failed to fetch: ${error instanceof Error ? error.message : "Unknown error"}`,
    };
  }
}

export function cacheWeather(
  city: string,
  data: WeatherData
): boolean {
  const cacheDir = process.env.CACHE_DIR || "/cache";
  const filePath = `${cacheDir}/${city}.json`;
  fs.writeFileSync(filePath, JSON.stringify(data));
  return true;
}

Troubleshooting

Build fails with type errors:
# Check TypeScript compilation
npx tsc --noEmit
Plugin too large (>5MB):
  • Review node_modules size
  • Remove unused dependencies
  • Use tree-shaking compatible libraries
Runtime errors with environment variables:
  • Verify .env file is created
  • Check noorle.yaml environment permissions
  • Ensure variable names are exact match

Next Steps