TypeScript Executor¶
Reference TypeScript implementation of Lace, conformant to spec version 0.9.0 (171/171 conformance vectors). Passes the same test suite as the canonical Python executor and is fully interchangeable.
The implementation is split into two packages following the packaging rule:
| Package | Repository | Description |
|---|---|---|
@lacelang/validator |
tracedown/lacelang-js-validator | Lexer, parser, semantic validator. Zero runtime dependencies. |
@lacelang/executor |
tracedown/lacelang-js-executor | HTTP runtime, assertion evaluation, cookie jars, extension dispatch. Depends on the validator. |
Requires Node.js 18+.
Installation¶
Not published to npm yet. Install from GitHub:
# 1. Install the validator (required dependency)
npm install git+https://github.com/tracedown/lacelang-js-validator.git
# 2. Install the executor
npm install git+https://github.com/tracedown/lacelang-js-executor.git
Or from local clones:
git clone https://github.com/tracedown/lacelang-js-validator.git
git clone https://github.com/tracedown/lacelang-js-executor.git
npm install ./lacelang-js-validator
npm install ./lacelang-js-executor
CLI usage¶
The executor CLI exposes three subcommands matching the testkit contract:
parse -- syntax check¶
Outputs the AST as JSON. Parsing is delegated to @lacelang/validator.
validate -- semantic checks¶
Runs the parser and semantic validator. Reports structured errors and warnings. No HTTP calls are made.
run -- full execution¶
Parses, validates, executes, and emits a ProbeResult JSON.
Run flags¶
| Flag | Description |
|---|---|
--vars <file> |
JSON object with script variable values ($var). |
--var KEY=VALUE |
Inject a single variable (repeatable, overrides --vars). VALUE is parsed as JSON when valid, otherwise kept as a string. |
--prev-results <file> |
Previous result JSON, making prev available in expressions. --prev is a short alias. |
--config <file> |
Explicit path to a lace.config TOML file. |
--env <name> |
Select [lace.config.<name>] section (overrides LACE_ENV). |
--enable-extension <name> |
Activate a built-in extension (repeatable). |
--save-to <path> |
Persist the result to disk. Directory: timestamped JSON. File: overwrite. "false": skip. |
--bodies-dir <path> |
Directory for request/response body files. |
--pretty |
Pretty-print the result JSON. |
Library API¶
LaceExecutor¶
The central entry point. Holds resolved config and registered extensions.
import { LaceExecutor } from "@lacelang/executor";
// Point to the lace/ directory -- config loaded once
const executor = new LaceExecutor("lace");
// Or override the config path directly
const executor = new LaceExecutor("lace", { env: "staging" });
Constructor options¶
| Parameter | Type | Default | Description |
|---|---|---|---|
root |
string \| null |
null |
Path to the lace/ directory. Discovers lace.config inside it and resolves script names relative to {root}/scripts/. |
config |
string \| null |
null |
Explicit path to lace.config (overrides root-based discovery). |
env |
string \| null |
null |
Selects [lace.config.{env}] section (overrides LACE_ENV). |
extensions |
string[] \| null |
null |
Built-in extensions to activate (e.g. ["laceNotifications"]). |
trackPrev |
boolean |
true |
Auto-store last result as prev for next run on each probe. |
LaceProbe¶
A prepared, reusable script bound to its parent executor. Created by
executor.probe().
// Prepare a probe by name -- resolves to lace/scripts/health/health.lace
const probe = executor.probe("health");
// Run -- returns a ProbeResult dict
const result = await probe.run({ base_url: "https://api.example.com" });
// Run again -- prev result from last run injected automatically
const result2 = await probe.run();
Script resolution¶
The script argument is resolved in order:
- Ends with
.lace-- treated as a file path {root}/scripts/{script}/{script}.laceexists -- name-based lookup- Exists as a file on disk -- read it
- Otherwise -- treated as inline Lace source code
Extensions¶
Built-in extensions are activated via config or constructor:
Register third-party extensions:
// Directory (finds myext.laceext + myext.config inside)
executor.extension("lace/extensions/myext");
// Explicit paths
executor.extension("path/to/custom.laceext", "path/to/custom.config");
One-shot execution¶
// No probe caching, no prev tracking
const result = await executor.run("lace/scripts/health/health.lace", { key: "val" });
// Inline source
const result = await executor.run(`
get("https://api.example.com/health")
.expect(status: 200)
`);
Low-level API¶
The stateless runScript() function is available for callers that need
full control over parsing, validation, and config:
import { parse } from "@lacelang/validator";
import { runScript, loadConfig } from "@lacelang/executor";
import * as fs from "node:fs";
const ast = parse(fs.readFileSync("script.lace", "utf-8"));
const config = loadConfig({ explicitPath: "lace.config" });
const result = await runScript(ast, { key: "val" }, null, null, null, null, null, config);
Configuration¶
There is exactly one config file per executor. The env parameter
selects a section within that file, not a different file.
# lace/lace.config
[executor]
maxRedirects = 10
maxTimeoutMs = 300000
# Staging overlay -- deep-merged on top of base
[lace.config.staging]
[lace.config.staging.executor]
maxTimeoutMs = 60000
Resolution by constructor arguments¶
| Constructor | Config file | Env overlay |
|---|---|---|
new LaceExecutor("lace") |
lace/lace.config |
none (base only) |
new LaceExecutor("lace", { env: "staging" }) |
lace/lace.config |
[lace.config.staging] merged on base |
new LaceExecutor(null, { config: "/path/lace.config", env: "prod" }) |
/path/lace.config |
[lace.config.prod] merged on base |
LACE_ENV=staging + new LaceExecutor("lace") |
lace/lace.config |
[lace.config.staging] (from env var) |
Return value¶
Both probe.run() and executor.run() return a Promise<Record<string, unknown>> matching
the ProbeResult wire format:
{
outcome: "success", // "success" | "failure" | "timeout"
startedAt: "2026-04-21T10:00:00.000Z",
endedAt: "2026-04-21T10:00:01.234Z",
elapsedMs: 1234,
runVars: {}, // run-scoped variables from .store()
calls: [...], // per-call result records
actions: {}, // write-back variables, notifications, etc.
}
User-Agent¶
Per spec section 3.6, this executor sets a
default User-Agent on outgoing requests:
Precedence (highest first): per-request headers: { "User-Agent": ... } --
lace.config [executor].user_agent -- the default above.