Skip to content

JavaScript Guide

Build Noorle plugins with JavaScript.

Why JavaScript?

Strengths

  • Familiar Syntax - Use the world's most popular programming language
  • Rapid Development - Fast iteration with dynamic typing
  • Native JSON Handling - Built-in JSON parsing and serialization

Best For

  • Quick prototypes and simple plugins
  • Developers comfortable with dynamic typing
  • Projects that prioritize development speed

Trade-offs

  • No compile-time type checking
  • Runtime errors instead of build-time errors

Quick Start

Prerequisites

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

Create Plugin

bash
# Initialize from template
noorle plugin init my-plugin --template javascript
cd my-plugin

# Install dependencies (JavaScript toolchain, wasm tools)
noorle plugin prepare

# Build and deploy
noorle plugin publish

Minimal Example

javascript
/**
 * Process input data
 * @param {string} input - The data to process
 * @returns {string} Processed result
 */
export function process(input) {
  if (!input) {
    throw new Error("Input cannot be empty");
  }

  const result = {
    original: input,
    processed: true,
    timestamp: new Date().toISOString()
  };

  return JSON.stringify(result, null, 2);
}

Project Structure

tree
my-plugin/
├── app.js                # Main implementation
├── wit/world.wit         # API definition
├── package.json          # Language dependencies
├── build.sh              # Build script
├── prepare.sh            # Dependency installer
├── noorle.yaml           # Plugin config (optional)
└── dist/                 # Build output
    ├── plugin.wasm       # Compiled component
    └── *.npack           # Deployment archive

File Descriptions

FilePurpose
app.jsPlugin implementation
wit/world.witWebAssembly interface definition
package.jsonDependencies and project config
build.shCompilation and component creation
prepare.shInstalls JavaScript toolchain and wasm tools

Implementation Guide

Basic Plugin Structure

javascript
// ES module configuration required
const CONFIG = {
  mode: 'development',
  verbose: true,
  version: '1.0.0'
};

function validateInput(input) {
  if (!input || typeof input !== 'string') {
    throw new Error("Input must be a non-empty string");
  }

  if (!input.trim()) {
    throw new Error("Input cannot be empty");
  }

  return true;
}

export function process(input) {
  validateInput(input);

  try {
    const data = JSON.parse(input);

    const result = {
      ...data,
      processed: true,
      timestamp: new Date().toISOString(),
      config: CONFIG
    };

    return JSON.stringify(result, null, 2);
  } catch (error) {
    throw new Error(`Processing failed: ${error.message}`);
  }
}

Working with WIT

Define your API in wit/world.wit:

wit
package example:my-plugin;

world my-plugin-component {
    /// Function documentation becomes tool description
    export process: func(input: string) -> result<string, string>;
}

JavaScript-specific binding:

javascript
// Export functions that match your WIT interface exactly
export function process(input) {
  // Implementation must match WIT signature
  return processData(input);
}

Error Handling

javascript
function safeJsonParse(input, fallback = null) {
  try {
    return JSON.parse(input);
  } catch (error) {
    if (fallback !== null) {
      return fallback;
    }
    throw new Error(`Invalid JSON: ${error.message}`);
  }
}

export function processData(input) {
  // Validate input type
  if (typeof input !== 'string') {
    throw new Error(`Expected string, got ${typeof input}`);
  }

  // Parse with error handling
  const data = safeJsonParse(input);

  if (!data || typeof data !== 'object') {
    throw new Error("Input must be a valid JSON object");
  }

  // Process with validation
  const result = {
    ...data,
    processed: true,
    validatedAt: new Date().toISOString()
  };

  return JSON.stringify(result);
}

Environment Variables

javascript
import { getEnvironment } from "wasi:cli/[email protected]";

function loadConfig() {
  const env = getEnvironment();
  const config = {};

  // Parse environment variables
  for (const [key, value] of env) {
    if (key === "API_KEY") {
      config.apiKey = value;
    } else if (key === "DEBUG") {
      config.debug = value === "true";
    } else if (key === "MAX_ITEMS") {
      config.maxItems = parseInt(value, 10) || 100;
    }
  }

  return config;
}

export function getConfiguration() {
  const config = loadConfig();

  return JSON.stringify({
    ...config,
    timestamp: new Date().toISOString(),
    loaded: true
  });
}

JSON Processing

javascript
function validateUserData(data) {
  const required = ['id', 'name', 'email'];
  const missing = required.filter(field => !(field in data));

  if (missing.length > 0) {
    throw new Error(`Missing required fields: ${missing.join(', ')}`);
  }

  if (typeof data.id !== 'number') {
    throw new Error("Field 'id' must be a number");
  }

  if (typeof data.name !== 'string') {
    throw new Error("Field 'name' must be a string");
  }

  return true;
}

export function processUser(input) {
  const userData = safeJsonParse(input);

  validateUserData(userData);

  const processedUser = {
    ...userData,
    processed: true,
    processedAt: new Date().toISOString(),
    valid: true
  };

  return JSON.stringify(processedUser, null, 2);
}

HTTP Requests (Optional)

Required WIT imports:

wit
import wasi:http/[email protected];
import wasi:http/[email protected];

Implementation:

javascript
// HTTP client example (simplified)
async function fetchData(url) {
  try {
    // Note: Actual WASI HTTP implementation required
    const response = await makeHttpRequest(url);

    return {
      data: response.data,
      status: response.status,
      success: true
    };
  } catch (error) {
    return {
      data: null,
      status: 500,
      success: false,
      error: error.message
    };
  }
}

export async function processWithHttp(input) {
  const { url } = JSON.parse(input);
  const result = await fetchData(url);

  return JSON.stringify(result);
}

Required permissions:

yaml
permissions:
  network:
    allow:
      - host: "api.example.com"

Testing

Local Testing with Wasmtime

bash
# Build the plugin
noorle plugin build

# Test with WAVE format
wasmtime run --invoke 'process("test input")' dist/plugin.wasm
wasmtime run --invoke 'add(1, 2)' dist/plugin.wasm
wasmtime run --invoke 'greet("World")' dist/plugin.wasm

# With environment variables
wasmtime run --env KEY=value --invoke 'process("test")' dist/plugin.wasm

Language-Specific Features

Class-Based Architecture

javascript
class DataProcessor {
  constructor(options = {}) {
    this.mode = options.mode || 'default';
    this.verbose = options.verbose || false;
    this.config = this.loadConfig();
  }

  loadConfig() {
    return {
      timeout: 5000,
      retries: 3,
      format: 'json'
    };
  }

  validate(input) {
    if (!input) {
      throw new Error("Input is required");
    }
    return true;
  }

  transform(data) {
    return {
      ...data,
      transformed: true,
      mode: this.mode,
      timestamp: new Date().toISOString()
    };
  }

  process(input) {
    this.validate(input);

    if (this.verbose) {
      console.log(`Processing in ${this.mode} mode`);
    }

    const parsed = JSON.parse(input);
    return this.transform(parsed);
  }
}

export function processWithClass(input) {
  const processor = new DataProcessor({
    mode: 'advanced',
    verbose: true
  });

  const result = processor.process(input);
  return JSON.stringify(result);
}

Functional Programming Patterns

javascript
// Higher-order functions for data processing
function createProcessor(transformFn) {
  return function(input) {
    const data = JSON.parse(input);
    const transformed = transformFn(data);
    return JSON.stringify(transformed);
  };
}

// Composition helpers
const pipe = (...fns) => (value) => fns.reduce((acc, fn) => fn(acc), value);

const addTimestamp = (data) => ({
  ...data,
  timestamp: new Date().toISOString()
});

const markProcessed = (data) => ({
  ...data,
  processed: true
});

const addMetadata = (data) => ({
  ...data,
  metadata: {
    version: '1.0.0',
    processor: 'javascript'
  }
});

export function processWithComposition(input) {
  const data = JSON.parse(input);

  const processData = pipe(
    addTimestamp,
    markProcessed,
    addMetadata
  );

  const result = processData(data);
  return JSON.stringify(result, null, 2);
}

Build Configuration

Dependencies

package.json:

json
{
  "type": "module",
  "dependencies": {
    "@bytecodealliance/componentize-js": "0.19.1",
    "@bytecodealliance/jco": "1.15.0"
  },
  "devDependencies": {
    "@types/node": "^18.0.0"
  }
}

Build Optimization

Optimize package.json for WebAssembly:

json
{
  "type": "module",
  "main": "app.js",
  "scripts": {
    "test": "node --test",
    "build": "./build.sh"
  }
}

Build Process

bash
# Build your plugin (handles compilation, optimization, packaging)
noorle plugin build

The build process:

  1. Prepares JavaScript runtime environment
  2. wkg fetches WIT dependencies from imports
  3. Creates component with JavaScript toolchain
  4. Optimizes output
  5. Packages into .npack archive

Troubleshooting

Common Issues

Build Fails

Problem: Node.js version compatibility

Solution:

bash
# Update to Node.js 18+
nvm install 18
nvm use 18

# Clear dependencies and reinstall
rm -rf node_modules package-lock.json
npm install

Runtime Error

Problem: JSON parsing failures

Solution:

javascript
function safeJsonProcess(input) {
  try {
    const data = JSON.parse(input);
    return processData(data);
  } catch (error) {
    throw new Error(`Invalid JSON input: ${error.message}`);
  }
}

JavaScript-Specific Issue

Problem: Module import errors

Solution:

bash
# Ensure package.json has ES module configuration
echo '{"type": "module"}' > package.json

# Check imports use .js extensions where needed
# import { helper } from './utils.js';

Debugging

Enable debug output:

bash
DEBUG=1 noorle plugin build

View WASM exports:

bash
wasm-tools component wit dist/plugin.wasm

Performance

Optimization tips:

  • Keep dependencies minimal for WASM compatibility
  • Use built-in JSON methods for best performance
  • Avoid large utility libraries

Best Practices

Do's

  • ✅ Use ES module syntax consistently
  • ✅ Validate all inputs thoroughly
  • ✅ Implement clear error messages
  • ✅ Leverage JavaScript's JSON strengths

Don'ts

  • ❌ Use Node.js-specific APIs
  • ❌ Import CommonJS modules
  • ❌ Assume input types without validation

Security

  • Validate all inputs
  • Use minimal permissions
  • Never log sensitive data
  • Sanitize error messages

Advanced Topics

Custom WASI Interfaces

wit
world my-plugin-component {
    import my-custom:interface/types@1.0.0;
    export process: func(input: string) -> result<string, string>;
}
javascript
// Import and use custom interfaces
import { customFunction } from "my-custom:interface/[email protected]";

export function useCustomInterface(input) {
  const data = JSON.parse(input);
  const result = customFunction(data);
  return JSON.stringify({ result });
}

Performance Monitoring

javascript
class PerformanceTracker {
  constructor() {
    this.metrics = new Map();
  }

  time(name, operation) {
    const startTime = Date.now();
    const result = operation();
    const duration = Date.now() - startTime;

    this.metrics.set(name, {
      duration,
      timestamp: new Date().toISOString()
    });

    console.log(`Operation ${name} took ${duration}ms`);
    return result;
  }

  getMetrics() {
    return Object.fromEntries(this.metrics);
  }
}

const tracker = new PerformanceTracker();

export function processWithMetrics(input) {
  return tracker.time('processData', () => {
    return processData(input);
  });
}

Working with Async Operations

javascript
// Handle async operations with proper error handling
export async function processAsync(input) {
  try {
    // Simulate async work
    await new Promise(resolve => setTimeout(resolve, 100));

    const data = JSON.parse(input);
    const result = {
      ...data,
      processed: true,
      processingTime: 100
    };

    return JSON.stringify(result);
  } catch (error) {
    throw new Error(`Async processing failed: ${error.message}`);
  }
}

Resources

Documentation

Tools

Examples

Next Steps

Continue Learning

Build Your Plugin