Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Using WCL in JavaScript / TypeScript

WCL runs in the browser and Node.js via WebAssembly. The wcl-wasm package provides parse, parseValues, and query functions with full TypeScript types.

Installation

npm install wcl-wasm

Initialization

The WASM module must be initialized once before use:

Browser / Deno:

import { init, parse, parseValues, query } from "wcl-wasm";

await init();

Node.js (ESM):

import { init, parse, parseValues, query } from "wcl-wasm";
import { readFileSync } from "node:fs";

await init(readFileSync(new URL("../node_modules/wcl-wasm/pkg/wcl_wasm_bg.wasm", import.meta.url)));

Node.js (CommonJS):

const { init, parse, parseValues, query } = require("wcl-wasm");
const fs = require("node:fs");
const path = require("node:path");

async function main() {
  await init(fs.readFileSync(path.join(__dirname, "../node_modules/wcl-wasm/pkg/wcl_wasm_bg.wasm")));
  const doc = parse('x = 42');
  console.log(doc.values);
}
main();

Calling any function before init() throws an error.

Parsing a WCL String

parse() runs the full 11-phase pipeline and returns a document object:

import { init, parse } from "wcl-wasm";

await init();

const doc = parse(`
  server web-prod {
    host = "0.0.0.0"
    port = 8080
    debug = false
  }
`);

if (doc.hasErrors) {
  for (const d of doc.diagnostics) {
    console.error(`${d.severity}: ${d.message}`);
  }
} else {
  console.log("Parsed successfully");
  console.log(doc.values);
}

The returned WclDocument has this shape:

interface WclDocument {
  values: Record<string, any>;      // evaluated top-level values
  hasErrors: boolean;                // true if any errors occurred
  diagnostics: WclDiagnostic[];     // all diagnostics
}

interface WclDiagnostic {
  severity: "error" | "warning";
  message: string;
  code?: string;                    // e.g. "E071" for type mismatch
}

Getting Just the Values

parseValues() returns only the evaluated values and throws on errors:

try {
  const values = parseValues(`
    name = "my-app"
    port = 8080
    tags = ["web", "prod"]
  `);

  console.log(values.name);  // "my-app"
  console.log(values.port);  // 8080
  console.log(values.tags);  // ["web", "prod"]
} catch (e) {
  console.error("Parse error:", e);
}

This is the simplest way to use WCL when you just want the config values and don’t need diagnostics.

Working with Blocks

Block values appear as objects with kind, id, attributes, and children:

const doc = parse(`
  server web-prod {
    host = "0.0.0.0"
    port = 8080
  }

  server web-staging {
    host = "staging.internal"
    port = 8081
  }
`);

// Blocks appear as values keyed by their type
// Use queries for structured access (see below)

Working with Tables

Tables evaluate to an array of row objects. Each row is an object mapping column names to cell values:

const doc = parse(`
  table users {
    name : string
    age  : int
    | "alice" | 25 |
    | "bob"   | 30 |
  }
`);

console.log(doc.values.users);
// [{ name: "alice", age: 25 }, { name: "bob", age: 30 }]

console.log(doc.values.users[0].name); // "alice"

Tables inside blocks appear in the block’s attributes:

const doc = parse(`
  service main {
    table config {
      key   : string
      value : int
      | "port" | 8080 |
    }
  }
`);

// Access via the block's values
console.log(doc.values.main.attributes.config);
// [{ key: "port", value: 8080 }]

Running Queries

query() parses a WCL string and executes a query in one call:

const result = query(`
  server svc-api {
    port = 8080
    env = "prod"
  }

  server svc-admin {
    port = 9090
    env = "prod"
  }

  server svc-debug {
    port = 3000
    env = "dev"
  }
`, "server | .port");

console.log(result);  // [8080, 9090, 3000]

More query examples:

// Select all server blocks
query(source, "server");

// Filter by attribute
query(source, 'server | .env == "prod"');

// Filter and project
query(source, 'server | .env == "prod" | .port');
// → [8080, 9090]

// Select by ID
query(source, "server#svc-api");

Throws if there are parse errors or if the query is invalid.

Custom Functions

Register custom JavaScript functions that are callable from WCL expressions:

const values = parseValues(`
  result = double(21)
  message = greet("World")
`, {
  functions: {
    double: (n: number) => n * 2,
    greet: (name: string) => `Hello, ${name}!`,
  },
});

console.log(values.result);   // 42
console.log(values.message);  // "Hello, World!"

Functions receive native JavaScript values and should return native values. Errors can be thrown normally:

const values = parseValues("result = safe_div(10, 0)", {
  functions: {
    safe_div: (a: number, b: number) => {
      if (b === 0) throw new Error("division by zero");
      return a / b;
    },
  },
});

In-Memory Files for Imports

Provide files as a map for import resolution without filesystem access (useful in browsers):

const doc = parse('import "utils.wcl"\nresult = base_port + 1', {
  files: {
    "utils.wcl": "base_port = 8080",
  },
});

console.log(doc.values.result);  // 8081

Custom Import Resolver

For more control over import resolution, provide a synchronous callback:

const doc = parse('import "config.wcl"', {
  importResolver: (path: string) => {
    if (path.endsWith("config.wcl")) {
      return 'port = 8080';
    }
    return null;  // file not found
  },
});

If both files and importResolver are provided, files takes precedence.

Parse Options

All options are optional:

interface ParseOptions {
  rootDir?: string;            // root directory for import resolution (default: ".")
  allowImports?: boolean;      // enable/disable imports (default: true)
  maxImportDepth?: number;     // max nested import depth (default: 32)
  maxMacroDepth?: number;      // max macro expansion depth (default: 64)
  maxLoopDepth?: number;       // max for-loop nesting (default: 32)
  maxIterations?: number;      // max total loop iterations (default: 10000)
  importResolver?: (path: string) => string | null;
  files?: Record<string, string>;
  functions?: Record<string, (...args: any[]) => any>;
}

When processing untrusted input, disable imports:

const doc = parse(untrustedInput, { allowImports: false });

Error Handling

Check hasErrors and inspect diagnostics:

const doc = parse(`
  server web {
    port = "not_a_number"
  }

  schema "server" {
    port: int
  }
`);

if (doc.hasErrors) {
  for (const d of doc.diagnostics) {
    const code = d.code ? `[${d.code}] ` : "";
    console.error(`${d.severity}: ${code}${d.message}`);
  }
}

parseValues() and query() throw on errors instead of returning them.

Complete Example

import { init, parse, parseValues, query } from "wcl-wasm";

await init();

// Simple value extraction
const config = parseValues(`
  app_name = "my-service"
  port = 8080
  debug = false
`);
console.log(`Starting ${config.app_name} on port ${config.port}`);

// Full document with validation
const doc = parse(`
  schema "server" {
    port: int
    host: string @optional
  }

  server svc-api {
    port = 8080
    host = "api.internal"
  }

  server svc-admin {
    port = 9090
    host = "admin.internal"
  }
`);

if (doc.hasErrors) {
  doc.diagnostics.forEach(d => console.error(d.message));
  process.exit(1);
}

// Query for all ports
const ports = query(doc_source, "server | .port");
console.log("All ports:", ports);  // [8080, 9090]

// Custom functions
const result = parseValues("doubled = double(21)", {
  functions: {
    double: (n) => n * 2,
  },
});
console.log(result.doubled);  // 42

Building from Source

To rebuild the WASM package from the Rust source:

# Build the WASM package
just build-wasm

# Run WASM tests
just test-wasm

This requires wasm-pack and the Rust toolchain.