---
title: "Run JavaScript ES modules in C#"
url: https://recursiveai.org/oneecma/cookbook/run-javascript-modules/
description: "Run JavaScript ES modules in C# with a runnable OneECMA example, published NuGet packages and the actual console output."
version: "1.1.1"
---

# Run JavaScript ES modules in C#

**OneEcma** is a free, open-source JavaScript interpreter for .NET from Recursive AI. MIT or Apache-2.0, no native dependencies.

[NuGet](https://www.nuget.org/packages/RecursiveAI.OneEcma) · [GitHub](https://github.com/recursive-ai-oss/oneecma-csharp) · [Docs](https://recursiveai.org/oneecma/docs/) · [Download example (.zip)](https://recursiveai.org/oneecma/cookbook/run-javascript-modules/run-javascript-modules.zip)

ES modules share JavaScript values with `import` and `export`. This example registers their source in memory; it does not load files or URLs.

```shell
dotnet add package RecursiveAI.OneEcma
```

Requires [.NET 10](https://dotnet.microsoft.com/download/dotnet/10.0) (net10.0); .NET 9 and earlier aren’t supported.

```csharp
using RecursiveAI.OneEcma;

var host = new InMemoryModuleHost()
    .Add("lib/util.js", "export const twice = x => x * 2;")
    .Add("main.js", "import { twice } from './lib/util.js'; export const answer = twice(21);");
var engine = new ScriptEngine(new ScriptEngineOptions { Host = host });
var exports = engine.EvaluateModule("main.js");
Console.WriteLine(engine.GetProperty(exports.AsObject!, "answer").AsNumber);
```

```text
42
```

Both module sources are inline: `lib/util.js` exports `twice`; `main.js` imports it and exports `answer`. An unresolved import fails rather than fetching a file or URL automatically.

Unzip [the example](https://recursiveai.org/oneecma/cookbook/run-javascript-modules/run-javascript-modules.zip), then `dotnet run`.

## Variations

To invoke an exported function from C#, replace the module setup and evaluation with:

```csharp
var host = new InMemoryModuleHost()
    .Add("lib/util.js", "export const twice = x => x * 2;")
    .Add("main.js", "export { twice } from './lib/util.js';");
var engine = new ScriptEngine(new ScriptEngineOptions { Host = host });
var exports = engine.EvaluateModule("main.js");
var twice = engine.GetProperty(exports.AsObject!, "twice");
Console.WriteLine(engine.Call(twice, default,
    [JsValue.FromNumber(9)]).AsNumber);
```

```text
18
```

**Read your own JavaScript files.** Save the two source strings from the snippet at the top as `lib/util.js` and `main.js` under your working directory. Register their contents explicitly:

```csharp
using RecursiveAI.OneEcma;

var host = new InMemoryModuleHost()
    .Add("lib/util.js", File.ReadAllText("lib/util.js"))
    .Add("main.js", File.ReadAllText("main.js"));
var engine = new ScriptEngine(new ScriptEngineOptions { Host = host });
var exports = engine.EvaluateModule("main.js");
Console.WriteLine(engine.GetProperty(exports.AsObject!, "answer").AsNumber);
```

```text
42
```

Only C# reads these two files; relative JavaScript imports still resolve through the registered module IDs, not arbitrary filesystem paths. The [host resolution contract](https://recursiveai.org/oneecma/docs/host-api/#modules) describes `ResolveModule` for a custom loader.

## Related recipes

- [Call a JavaScript function from C#](https://recursiveai.org/oneecma/cookbook/call-javascript-function/)
- [Use JavaScript source maps](https://recursiveai.org/oneecma/cookbook/source-maps/)

## The short answer

Register source strings in `InMemoryModuleHost`, pass that host through `ScriptEngineOptions`, and call `EvaluateModule` with the entry module’s ID. The returned `JsValue` is the module namespace. Read an exported value with `GetProperty(namespace.AsObject!, name)` or retrieve an exported function and invoke it with `Call`.

## What are ES modules in JavaScript?

ES modules separate source into units with explicit imports and exports. In this example, one module owns a reusable calculation while a second imports it and publishes the result. The names `lib/util.js` and `main.js` are identifiers in the host’s dictionary, not files created by the example.

The module source is ordinary JavaScript: `export const twice = x => x * 2;` exports a function, and `import { twice } from './lib/util.js'` imports that named function. Calling it with 21 produces the exported `answer` value, 42. C# does not need to rewrite the import into a global variable or concatenate the modules into a single script.

The [JavaScript module guide](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules) describes the language’s import and export syntax. The host controls how source is found; language syntax by itself does not supply a browser or server runtime inside your .NET application.

## How do I run a JavaScript module from C#?

Use `EvaluateModule`, not the plain script evaluation entry point, for source containing module imports and exports. Supply a host whose `ResolveModule` method can return a module ID and its source. `InMemoryModuleHost` provides that implementation for strings registered with `Add`.

Register every dependency before evaluating the entry module. The entry is resolved through the host too, so it needs its own registration. A relative import beginning with `./` or `../` is resolved against the importing module’s slash-separated ID. In the example, `main.js` plus `./lib/util.js` resolves to `lib/util.js`. IDs use ordinal string comparison, so keep their spelling and case consistent.

The [OneECMA host API](https://recursiveai.org/oneecma/docs/host-api/) documents this boundary. For an application-defined loader, implement the host resolution contract. The recipe does not grant file or network access, search installed packages, or infer a package installation step from an import name.

## How do I import a JavaScript function?

Import it in JavaScript when another module needs to use it. Export or re-export it from your entry module when C# needs to call it. The variation re-exports `twice` and then retrieves that function from the namespace object. `engine.Call` receives the function, a receiver value and a list of JavaScript argument values.

`JsValue.FromNumber(9)` supplies the numeric argument; the returned value’s `AsNumber` reads the result, 18. This keeps the boundary explicit rather than assuming a JavaScript function is a C# delegate. If you need a JavaScript string or object instead, use the corresponding value representation and inspect the appropriate result type.

Keep module loading separate from application permissions. Registering trusted source strings is enough for this small calculation. If you later replace the in-memory host with a loader, decide which IDs it accepts and where it may read before exposing it to scripts. Relative import syntax is a resolution rule, not a permission to read arbitrary paths.
