Install OneEcma for .NET 10, evaluate JavaScript from C#, pass values and functions in, read the error report when a script fails, run untrusted code under limits, and check source without running it.
Install
OneEcma targets net10.0, so you need the .NET 10 SDK. The packages share one version; this guide uses 1.0.0.
dotnet new console -o OneEcmaStart cd OneEcmaStart dotnet add package RecursiveAI.OneEcma --version 1.0.0
RecursiveAI.OneEcma is the whole engine, Intl included, with no dependency beyond the .NET base class library. It is NativeAOT- and trim-compatible. Two more packages are optional:
RecursiveAI.OneEcma.Generatorswrites host bindings for .NET types marked[JsExport]at build time. See Expose .NET objects with [JsExport].RecursiveAI.OneEcma.Cliis theoneecmacommand:dotnet tool install -g RecursiveAI.OneEcma.Cli. See The oneecma command.
The engine has no file, network, process or environment access of its own. A script reaches only what your code hands it.
The snippets below are one program, in order. Put these using directives at the top of Program.cs:
using RecursiveAI.OneEcma; using RecursiveAI.OneEcma.Diagnostics; using RecursiveAI.OneEcma.Syntax;
Evaluate a script
ScriptEngine is one engine with one global scope. Evaluate runs a script and returns its completion value as a JsValue:
var engine = new ScriptEngine(); JsValue result = engine.Evaluate("[1, 2, 3].map(n => n * 2).join('-')", "sum.js"); Console.WriteLine(engine.ToDisplayString(result)); Console.WriteLine(engine.Evaluate("6 * 7").AsNumber);
2-4-6 42
The second argument is the file name reports and err.stack use. An engine isn’t thread-safe: use one per thread.
Pass values and functions in
Values go onto the global object through the engine’s realm. Put that setup in ScriptEngineOptions.Configure: when a script fails, the engine replays the run to explain it, and the replay calls Configure again to rebuild the same world.
var shop = new ScriptEngine(new ScriptEngineOptions { Configure = e => { JsRealm realm = e.MainRealm; JsObject order = realm.NewObject(); realm.DefineData(order, "id", JsValue.FromNumber(1042)); realm.DefineData(order, "items", JsValue.FromObject(realm.NewArray([JsValue.FromNumber(4), JsValue.FromNumber(6)]))); realm.DefineData(realm.Global, "order", JsValue.FromObject(order)); realm.DefineData(realm.Global, "tax", JsValue.FromObject( realm.NewFunction("tax", 1, (_, args) => JsValue.FromNumber(Math.Round(args[0].AsNumber * 0.08, 2)))), JsAttributes.Method); }, }); JsValue total = shop.Evaluate("const subtotal = order.items.reduce((a, b) => a + b, 0);\nsubtotal + tax(subtotal)", "total.js"); Console.WriteLine($"total: {total.AsNumber}");
total: 10.8A host function is (this, args) => JsValue. For whole .NET classes, records and enums, let the generator write the bindings: see Expose .NET objects with [JsExport]. For tasks, exceptions in both directions and reading script objects back, see Call C# from JavaScript.
Read the report when a script fails
A script error reaches C# as a JsException. Message is what the script would see as `${err.name}: ${err.message}`, and Report is the structured diagnostic; DiagnosticRenderer.Render prints it:
try { shop.Evaluate("const count = order.item.length;", "count.js"); } catch (JsException e) { Console.WriteLine(e.Message); Console.WriteLine(DiagnosticRenderer.Render(e.Report!)); Console.WriteLine(shop.ToDisplayString(shop.MainRealm.Get(e.Value.AsObject!, "stack"))); }
TypeError: Cannot read properties of undefined (reading 'length') error[TypeError]: Cannot read properties of undefined (reading 'length') --> count.js:1:21 | 1 | const count = order.item.length; | ^^^^ `order.item` is undefined | = help: did you mean `order.items`? (Array, 2 items) = stack: <top-level> (count.js:1) TypeError: Cannot read properties of undefined (reading 'length') at count.js:1:26
The headline and err.stack are V8’s: Node prints the same two lines for this script. The underline, the label and the suggestion come from a replay of the failed run with tracing on, so a run that succeeds pays nothing for them. The suggestion only ever names something the object actually holds.
The same report comes as a compact rendering for a model’s context and as versioned JSON: see Error reports: text, compact and JSON.
Run untrusted code under limits
EngineLimits bounds a run. When a limit is hit the run ends with a JsLimitException, which script can’t catch, and its report says which limit and where:
var sandbox = new ScriptEngine(new ScriptEngineOptions { Limits = new EngineLimits { Fuel = 1_000_000, WallClockMs = 2_000, MaxCallDepth = 2_000 }, }); try { sandbox.Evaluate("let n = 0;\nwhile (true) { n++; }", "spin.js"); } catch (JsLimitException e) { Console.WriteLine($"{e.Kind}: {e.Message}"); Console.WriteLine(DiagnosticRenderer.Render(e.Report!)); }
Fuel: Execution exceeded the step limit of 1000000 steps error[Limit]: Execution exceeded the step limit of 1000000 steps --> spin.js:2:1 | 2 | while (true) { n++; } | ^^^^^^^^^^^^^^^^^^^^^ | = stack: <top-level> (spin.js:2)
Fuel is charged per call, per loop back-edge, per step of RegExp backtracking and per unit of work in long-running built-ins. The defaults are generous (4 billion steps, 2 GiB of allocation, no wall-clock limit), so set them for anything you don’t trust. Every limit, cancellation included, is in Run untrusted JavaScript with limits.
Check source without running it
ScriptEngine.Check parses and runs the early errors only, and returns every error, not just the first:
foreach (Diagnostic d in ScriptEngine.Check("let a = 1;\nlet a = 2;\nlet b = ;", new ParseOptions { FileName = "two.js" })) { Console.WriteLine($"{d.Position.Line}:{d.Position.Column} {d.Code}: {d.Headline}"); }
2:5 redeclaration: Identifier 'a' has already been declared 3:9 unexpected-token: Unexpected token ';'
Code is a stable identifier that is never reworded. ScriptEngine.CheckAndRepair also applies the syntax-only repairs that have a single plausible fix: see Run code a model wrote.
Next
- Run untrusted JavaScript with limits
- Expose .NET objects with [JsExport]
- Error reports: text, compact and JSON
- The host API, the reference for everything above
- What is supported and how it is checked