Browse Source

Support ECMAScript modules (export/import statements, modules definition) (#1054)

Christian Rondeau 3 years ago
parent
commit
fcc7c8d257
42 changed files with 1147 additions and 181 deletions
  1. 3 2
      Jint.Tests.Test262/Language/ModuleTests.cs
  2. 1 1
      Jint.Tests.Test262/Test262Test.cs
  3. 1 5
      Jint.Tests/Jint.Tests.csproj
  4. 249 0
      Jint.Tests/Runtime/ModuleTests.cs
  5. 51 0
      Jint.Tests/Runtime/Modules/DefaultModuleResolverTests.cs
  6. 3 0
      Jint.Tests/Runtime/Scripts/modules/format-name.js
  7. 14 0
      Jint.Tests/Runtime/Scripts/modules/user.js
  8. 119 11
      Jint/Engine.Modules.cs
  9. 1 0
      Jint/Engine.cs
  10. 1 1
      Jint/HoistingScope.cs
  11. 91 0
      Jint/ModuleBuilder.cs
  12. 1 0
      Jint/Native/Function/FunctionInstance.cs
  13. 1 2
      Jint/Native/Function/ScriptFunctionInstance.cs
  14. 1 1
      Jint/Native/ICallable.cs
  15. 2 2
      Jint/Native/Object/ObjectInstance.cs
  16. 10 6
      Jint/Native/Promise/PromiseOperations.cs
  17. 7 15
      Jint/Options.Extensions.cs
  18. 16 30
      Jint/Options.cs
  19. 5 3
      Jint/Runtime/Environments/ExecutionContext.cs
  20. 1 1
      Jint/Runtime/Environments/FunctionEnvironmentRecord.cs
  21. 8 7
      Jint/Runtime/Environments/ModuleEnvironmentRecord.cs
  22. 15 1
      Jint/Runtime/ExceptionHelper.cs
  23. 17 1
      Jint/Runtime/ExecutionContextStack.cs
  24. 4 3
      Jint/Runtime/Host.cs
  25. 20 0
      Jint/Runtime/IScriptOrModule.Extensions.cs
  26. 5 0
      Jint/Runtime/IScriptOrModule.cs
  27. 5 3
      Jint/Runtime/Interpreter/JintStatementList.cs
  28. 21 0
      Jint/Runtime/Interpreter/Statements/JintExportAllDeclaration.cs
  29. 31 0
      Jint/Runtime/Interpreter/Statements/JintExportDefaultDeclaration.cs
  30. 98 0
      Jint/Runtime/Interpreter/Statements/JintExportNamedDeclaration.cs
  31. 29 0
      Jint/Runtime/Interpreter/Statements/JintImportDeclaration.cs
  32. 4 4
      Jint/Runtime/Interpreter/Statements/JintStatement.cs
  33. 7 0
      Jint/Runtime/JavaScriptException.cs
  34. 111 26
      Jint/Runtime/Modules/DefaultModuleLoader.cs
  35. 19 2
      Jint/Runtime/Modules/FailFastModuleLoader.cs
  36. 7 8
      Jint/Runtime/Modules/IModuleLoader.cs
  37. 83 45
      Jint/Runtime/Modules/JsModule.cs
  38. 1 0
      Jint/Runtime/Modules/ModuleNamespace.cs
  39. 16 0
      Jint/Runtime/Modules/ModuleResolutionException.cs
  40. 7 0
      Jint/Runtime/Modules/ResolvedSpecifier.cs
  41. 8 0
      Jint/Runtime/Modules/SpecifierType.cs
  42. 53 1
      README.md

+ 3 - 2
Jint.Tests.Test262/Language/ModuleTests.cs

@@ -1,6 +1,8 @@
 using Jint.Runtime;
 using Jint.Runtime.Modules;
 using System;
+using System.IO;
+using System.Reflection;
 using Xunit;
 using Xunit.Sdk;
 
@@ -43,8 +45,7 @@ public class ModuleTests : Test262Test
 
         var options = new Options();
         options.Host.Factory = _ => new ModuleTestHost();
-        options.Modules.Enabled = true;
-        options.WithModuleLoader(new DefaultModuleLoader(null));
+        options.EnableModules(Path.Combine(BasePath, "test"));
 
         var engine = new Engine(options);
 

+ 1 - 1
Jint.Tests.Test262/Test262Test.cs

@@ -22,7 +22,7 @@ namespace Jint.Tests.Test262
     {
         private static readonly Dictionary<string, Script> Sources;
 
-        private static readonly string BasePath;
+        protected static readonly string BasePath;
 
         private static readonly TimeZoneInfo _pacificTimeZone;
 

+ 1 - 5
Jint.Tests/Jint.Tests.csproj

@@ -5,11 +5,7 @@
     <AssemblyOriginatorKeyFile>..\Jint\Jint.snk</AssemblyOriginatorKeyFile>
     <SignAssembly>true</SignAssembly>
     <IsPackable>false</IsPackable>
-    <!--
-      Unity currently supports only C# 8 so ensure we can use the features Jint introduces
-      https://docs.unity3d.com/Manual/CSharpCompiler.html
-    -->
-    <LangVersion>8</LangVersion>
+    <LangVersion>latest</LangVersion>
     <NoWarn>612</NoWarn>
   </PropertyGroup>
   <ItemGroup>

+ 249 - 0
Jint.Tests/Runtime/ModuleTests.cs

@@ -0,0 +1,249 @@
+#if(NET6_0_OR_GREATER)
+using System.IO;
+using System.Reflection;
+#endif
+using System;
+using Jint.Native;
+using Jint.Runtime;
+using Xunit;
+
+namespace Jint.Tests.Runtime;
+
+public class ModuleTests
+{
+    private readonly Engine _engine;
+
+    public ModuleTests()
+    {
+        _engine = new Engine();
+    }
+
+    [Fact]
+    public void ShouldExportNamed()
+    {
+        _engine.AddModule("my-module", @"export const value = 'exported value';");
+        var ns = _engine.ImportModule("my-module");
+
+        Assert.Equal("exported value", ns.Get("value").AsString());
+    }
+
+    [Fact]
+    public void ShouldExportNamedListRenamed()
+    {
+        _engine.AddModule("my-module", @"const value1 = 1; const value2 = 2; export { value1 as renamed1, value2 as renamed2 }");
+        var ns = _engine.ImportModule("my-module");
+
+        Assert.Equal(1, ns.Get("renamed1").AsInteger());
+        Assert.Equal(2, ns.Get("renamed2").AsInteger());
+    }
+
+    [Fact]
+    public void ShouldExportDefault()
+    {
+        _engine.AddModule("my-module", @"export default 'exported value';");
+        var ns = _engine.ImportModule("my-module");
+
+        Assert.Equal("exported value", ns.Get("default").AsString());
+    }
+
+    [Fact]
+    public void ShouldExportAll()
+    {
+        _engine.AddModule("module1", @"export const value = 'exported value';");
+        _engine.AddModule("module2", @"export * from 'module1';");
+        var ns = _engine.ImportModule("module2");
+
+        Assert.Equal("exported value", ns.Get("value").AsString());
+    }
+
+    [Fact]
+    public void ShouldImportNamed()
+    {
+        _engine.AddModule("imported-module", @"export const value = 'exported value';");
+        _engine.AddModule("my-module", @"import { value } from 'imported-module'; export const exported = value;");
+        var ns = _engine.ImportModule("my-module");
+
+        Assert.Equal("exported value", ns.Get("exported").AsString());
+    }
+
+    [Fact]
+    public void ShouldImportRenamed()
+    {
+        _engine.AddModule("imported-module", @"export const value = 'exported value';");
+        _engine.AddModule("my-module", @"import { value as renamed } from 'imported-module'; export const exported = renamed;");
+        var ns = _engine.ImportModule("my-module");
+
+        Assert.Equal("exported value", ns.Get("exported").AsString());
+    }
+
+    [Fact]
+    public void ShouldImportDefault()
+    {
+        _engine.AddModule("imported-module", @"export default 'exported value';");
+        _engine.AddModule("my-module", @"import imported from 'imported-module'; export const exported = imported;");
+        var ns = _engine.ImportModule("my-module");
+
+        Assert.Equal("exported value", ns.Get("exported").AsString());
+    }
+
+    [Fact]
+    public void ShouldImportAll()
+    {
+        _engine.AddModule("imported-module", @"export const value = 'exported value';");
+        _engine.AddModule("my-module", @"import * as imported from 'imported-module'; export const exported = imported.value;");
+        var ns = _engine.ImportModule("my-module");
+
+        Assert.Equal("exported value", ns.Get("exported").AsString());
+    }
+
+    [Fact]
+    public void ShouldPropagateThrowStatementOnCSharpImport()
+    {
+        _engine.AddModule("my-module", @"throw new Error('imported successfully');");
+
+        var exc = Assert.Throws<JavaScriptException>(() => _engine.ImportModule("my-module"));
+        Assert.Equal("imported successfully", exc.Message);
+        Assert.Equal("my-module", exc.Location.Source);
+    }
+
+    [Fact]
+    public void ShouldPropagateThrowStatementThroughJavaScriptImport()
+    {
+        _engine.AddModule("imported-module", @"throw new Error('imported successfully');");
+        _engine.AddModule("my-module", @"import 'imported-module';");
+
+        var exc = Assert.Throws<JavaScriptException>(() => _engine.ImportModule("my-module"));
+        Assert.Equal("imported successfully", exc.Message);
+        Assert.Equal("imported-module", exc.Location.Source);
+    }
+
+    [Fact]
+    public void ShouldAddModuleFromJsValue()
+    {
+        _engine.AddModule("my-module", builder => builder.ExportValue("value", JsString.Create("hello world")));
+        var ns = _engine.ImportModule("my-module");
+
+        Assert.Equal("hello world", ns.Get("value").AsString());
+    }
+
+    [Fact]
+    public void ShouldAddModuleFromClrInstance()
+    {
+        _engine.AddModule("imported-module", builder => builder.ExportObject("value", new ImportedClass { Value = "instance value" }));
+        _engine.AddModule("my-module", @"import { value } from 'imported-module'; export const exported = value.value;");
+        var ns = _engine.ImportModule("my-module");
+
+        Assert.Equal("instance value", ns.Get("exported").AsString());
+    }
+
+    [Fact]
+    public void ShouldAllowInvokeUserDefinedClass()
+    {
+        _engine.AddModule("user", "export class UserDefined { constructor(v) { this._v = v; } hello(c) { return `hello ${this._v}${c}`; } }");
+        var ctor = _engine.ImportModule("user").Get("UserDefined");
+        var instance = _engine.Construct(ctor, JsString.Create("world"));
+        var result = instance.GetMethod("hello").Call(instance, JsString.Create("!"));
+
+        Assert.Equal("hello world!", result);
+    }
+
+    [Fact]
+    public void ShouldAddModuleFromClrType()
+    {
+        _engine.AddModule("imported-module", builder => builder.ExportType<ImportedClass>());
+        _engine.AddModule("my-module", @"import { ImportedClass } from 'imported-module'; export const exported = new ImportedClass().value;");
+        var ns = _engine.ImportModule("my-module");
+
+        Assert.Equal("hello world", ns.Get("exported").AsString());
+    }
+
+    private class ImportedClass
+    {
+        public string Value { get; set; } = "hello world";
+    }
+
+    [Fact]
+    public void ShouldAllowExportMultipleImports()
+    {
+        _engine.AddModule("@mine/import1", builder => builder.ExportValue("value1", JsNumber.Create(1)));
+        _engine.AddModule("@mine/import2", builder => builder.ExportValue("value2", JsNumber.Create(2)));
+        _engine.AddModule("@mine", "export * from '@mine/import1'; export * from '@mine/import2'");
+        _engine.AddModule("app", @"import { value1, value2 } from '@mine'; export const result = `${value1} ${value2}`");
+        var ns = _engine.ImportModule("app");
+
+        Assert.Equal("1 2", ns.Get("result").AsString());
+    }
+
+    /* ECMAScript 2020 "export * as ns from"
+    [Fact]
+    public void ShouldAllowNamedStarExport()
+    {
+        _engine.AddModule("imported-module", builder => builder.ExportValue("value1", 5));
+        _engine.AddModule("my-module", "export * as ns from 'imported-module';");
+        var ns = _engine.ImportModule("my-module");
+
+        Assert.Equal(5, ns.Get("ns").Get("value1").AsNumber());
+    }
+    */
+
+    [Fact]
+    public void ShouldAllowChaining()
+    {
+        _engine.AddModule("dependent-module", "export const dependency = 1;");
+        _engine.AddModule("my-module", builder => builder
+            .AddSource("import { dependency } from 'dependent-module';")
+            .AddSource("export const output = dependency + 1;")
+            .ExportValue("num", JsNumber.Create(-1))
+        );
+        var ns = _engine.ImportModule("my-module");
+
+        Assert.Equal(2, ns.Get("output").AsInteger());
+        Assert.Equal(-1, ns.Get("num").AsInteger());
+    }
+
+    [Fact]
+    public void ShouldAllowLoadingMoreThanOnce()
+    {
+        var called = 0;
+        _engine.AddModule("imported-module", builder => builder.ExportFunction("count", args => called++));
+        _engine.AddModule("my-module", @"import { count } from 'imported-module'; count();");
+        _engine.ImportModule("my-module");
+        _engine.ImportModule("my-module");
+
+        Assert.Equal(called, 1);
+    }
+
+#if(NET6_0_OR_GREATER)
+
+    [Fact]
+    public void CanLoadModuleImportsFromFiles()
+    {
+        var engine = new Engine(options => options.EnableModules(GetBasePath()));
+        engine.AddModule("my-module", "import { User } from './modules/user.js'; export const user = new User('John', 'Doe');");
+        var ns = engine.ImportModule("my-module");
+
+        Assert.Equal("John Doe", ns["user"].Get("name").AsString());
+    }
+
+    [Fact]
+    public void CanImportFromFile()
+    {
+        var engine = new Engine(options => options.EnableModules(GetBasePath()));
+        var ns = engine.ImportModule("./modules/format-name.js");
+        var result = engine.Invoke(ns.Get("formatName"), "John", "Doe").AsString();
+
+        Assert.Equal("John Doe", result);
+    }
+
+    private static string GetBasePath()
+    {
+        var assemblyPath = new Uri(typeof(ModuleTests).GetTypeInfo().Assembly.Location).LocalPath;
+        var assemblyDirectory = new FileInfo(assemblyPath).Directory;
+        return Path.Combine(
+            assemblyDirectory?.Parent?.Parent?.Parent?.FullName ?? throw new NullReferenceException("Could not find tests base path"),
+            "Runtime",
+            "Scripts");
+    }
+
+#endif
+}

+ 51 - 0
Jint.Tests/Runtime/Modules/DefaultModuleResolverTests.cs

@@ -0,0 +1,51 @@
+using Jint.Runtime.Modules;
+using Xunit;
+
+namespace Jint.Tests.Runtime.Modules;
+
+public class DefaultModuleLoaderTests
+{
+    [Theory]
+    [InlineData("./other.js", @"file:///project/folder/other.js")]
+    [InlineData("../model/other.js", @"file:///project/model/other.js")]
+    [InlineData("/project/model/other.js", @"file:///project/model/other.js")]
+    [InlineData("file:///project/model/other.js", @"file:///project/model/other.js")]
+    public void ShouldResolveRelativePaths(string specifier, string expectedUri)
+    {
+        var resolver = new DefaultModuleLoader("file:///project");
+
+        var resolved = resolver.Resolve("file:///project/folder/script.js", specifier);
+
+        Assert.Equal(specifier, resolved.Specifier);
+        Assert.Equal(expectedUri, resolved.Key);
+        Assert.Equal(expectedUri, resolved.Uri?.AbsoluteUri);
+        Assert.Equal(SpecifierType.RelativeOrAbsolute, resolved.Type);
+    }
+
+    [Theory]
+    [InlineData("./../../other.js")]
+    [InlineData("../../model/other.js")]
+    [InlineData("/model/other.js")]
+    [InlineData("file:///etc/secret.js")]
+    public void ShouldRejectPathsOutsideOfBasePath(string specifier)
+    {
+        var resolver = new DefaultModuleLoader("file:///project");
+
+        var exc = Assert.Throws<ModuleResolutionException>(() => resolver.Resolve("file:///project/folder/script.js", specifier));
+        Assert.StartsWith(exc.ResolverAlgorithmError, "Unauthorized Module Path");
+        Assert.StartsWith(exc.Specifier, specifier);
+    }
+
+    [Fact]
+    public void ShouldResolveBareSpecifiers()
+    {
+        var resolver = new DefaultModuleLoader("/");
+
+        var resolved = resolver.Resolve(null, "my-module");
+
+        Assert.Equal("my-module", resolved.Specifier);
+        Assert.Equal("my-module", resolved.Key);
+        Assert.Equal(null, resolved.Uri?.AbsoluteUri);
+        Assert.Equal(SpecifierType.Bare, resolved.Type);
+    }
+}

+ 3 - 0
Jint.Tests/Runtime/Scripts/modules/format-name.js

@@ -0,0 +1,3 @@
+export function formatName(firstName, lastName) {
+    return `${firstName} ${lastName}`;
+}

+ 14 - 0
Jint.Tests/Runtime/Scripts/modules/user.js

@@ -0,0 +1,14 @@
+import { formatName as nameFormatter } from './format-name.js';
+
+class User {
+    constructor(firstName, lastName) {
+        this._firstName = firstName;
+        this._lastName = lastName;
+    }
+
+    get name() {
+        return nameFormatter(this._firstName, this._lastName);
+    }
+}
+
+export { User };

+ 119 - 11
Jint/Engine.Modules.cs

@@ -1,4 +1,13 @@
-using System.Collections.Generic;
+#nullable enable
+
+using System;
+using System.Collections.Generic;
+using Esprima;
+using Jint.Native;
+using Jint.Native.Object;
+using Jint.Native.Promise;
+using Jint.Runtime;
+using Jint.Runtime.Interpreter;
 using Jint.Runtime.Modules;
 
 namespace Jint
@@ -7,27 +16,126 @@ namespace Jint
     {
         internal IModuleLoader ModuleLoader { get; set; }
 
-        private readonly Dictionary<ModuleCacheKey, JsModule> _modules = new();
+        private readonly Dictionary<string, JsModule> _modules = new();
+        private readonly Dictionary<string, ModuleBuilder> _builders = new();
 
-        public JsModule LoadModule(string specifier) => LoadModule(null, specifier);
+        /// <summary>
+        /// https://tc39.es/ecma262/#sec-getactivescriptormodule
+        /// </summary>
+        internal IScriptOrModule? GetActiveScriptOrModule()
+        {
+            return _executionContexts?.GetActiveScriptOrModule();
+        }
 
-        internal JsModule LoadModule(string referencingModuleLocation, string specifier)
+        internal JsModule LoadModule(string specifier) => LoadModule(null, specifier);
+
+        internal JsModule LoadModule(string? referencingModuleLocation, string specifier)
         {
-            var key = new ModuleCacheKey(referencingModuleLocation ?? string.Empty, specifier);
+            var moduleResolution = ModuleLoader.Resolve(referencingModuleLocation, specifier);
 
-            if (_modules.TryGetValue(key, out var module))
+            if (_modules.TryGetValue(moduleResolution.Key, out var module))
             {
                 return module;
             }
 
-            var (loadedModule, location) = ModuleLoader.LoadModule(this, specifier, referencingModuleLocation);
-            module = new JsModule(this, _host.CreateRealm(), loadedModule, location.AbsoluteUri, false);
+            if (_builders.TryGetValue(specifier, out var moduleBuilder))
+            {
+                var parsedModule = moduleBuilder.Parse();
+                module = new JsModule(this, _host.CreateRealm(), parsedModule, null, false);
+                // Early link is required because we need to bind values before returning
+                module.Link();
+                moduleBuilder.BindExportedValues(module);
+                _builders.Remove(specifier);
+            }
+            else
+            {
+                var parsedModule = ModuleLoader.LoadModule(this, moduleResolution);
+                module = new JsModule(this, _host.CreateRealm(), parsedModule, moduleResolution.Uri?.LocalPath, false);
+            }
 
-            _modules[key] = module;
+            _modules[moduleResolution.Key] = module;
 
             return module;
         }
 
-        internal readonly record struct ModuleCacheKey(string ReferencingModuleLocation, string Specifier);
+        public void AddModule(string specifier, string source)
+        {
+            var moduleBuilder = new ModuleBuilder(this);
+            moduleBuilder.AddSource(source);
+            AddModule(specifier, moduleBuilder);
+        }
+
+        public void AddModule(string specifier, Action<ModuleBuilder> buildModule)
+        {
+            var moduleBuilder = new ModuleBuilder(this);
+            buildModule(moduleBuilder);
+            AddModule(specifier, moduleBuilder);
+        }
+
+        public void AddModule(string specifier, ModuleBuilder moduleBuilder)
+        {
+            _builders.Add(specifier, moduleBuilder);
+        }
+
+        public ObjectInstance ImportModule(string specifier)
+        {
+            var moduleResolution = ModuleLoader.Resolve(null, specifier);
+
+            if (!_modules.TryGetValue(moduleResolution.Key, out var module))
+            {
+                module = LoadModule(null, specifier);
+            }
+
+            if (module.Status == ModuleStatus.Unlinked)
+            {
+                module.Link();
+            }
+
+            if (module.Status == ModuleStatus.Linked)
+            {
+                var ownsContext = _activeEvaluationContext is null;
+                _activeEvaluationContext ??= new EvaluationContext(this);
+                JsValue evaluationResult;
+                try
+                {
+                    evaluationResult = module.Evaluate();
+                }
+                finally
+                {
+                    if (ownsContext)
+                    {
+                        _activeEvaluationContext = null;
+                    }
+                }
+
+                if (evaluationResult == null)
+                {
+                    ExceptionHelper.ThrowInvalidOperationException($"Error while evaluating module: Module evaluation did not return a promise");
+                }
+                else if (evaluationResult is not PromiseInstance promise)
+                {
+                    ExceptionHelper.ThrowInvalidOperationException($"Error while evaluating module: Module evaluation did not return a promise: {evaluationResult.Type}");
+                }
+                else if (promise.State == PromiseState.Rejected)
+                {
+                    ExceptionHelper.ThrowJavaScriptException(this, promise.Value, new Completion(CompletionType.Throw, promise.Value, null, new Location(new Position(), new Position(), specifier)));
+                }
+                else if (promise.State != PromiseState.Fulfilled)
+                {
+                    ExceptionHelper.ThrowInvalidOperationException($"Error while evaluating module: Module evaluation did not return a fulfilled promise: {promise.State}");
+                }
+            }
+
+            if (module.Status == ModuleStatus.Evaluated)
+            {
+                // TODO what about callstack and thrown exceptions?
+                RunAvailableContinuations(_eventLoop);
+
+                return JsModule.GetModuleNamespace(module);
+            }
+
+            ExceptionHelper.ThrowNotSupportedException($"Error while evaluating module: Module is in an invalid state: '{module.Status}'");
+            return default;
+        }
     }
-}
+}

+ 1 - 0
Jint/Engine.cs

@@ -157,6 +157,7 @@ namespace Jint
             PrivateEnvironmentRecord privateEnvironment)
         {
             var context = new ExecutionContext(
+                null,
                 lexicalEnvironment,
                 variableEnvironment,
                 privateEnvironment,

+ 1 - 1
Jint/HoistingScope.cs

@@ -246,7 +246,7 @@ namespace Jint
                             }
                         }
 
-                        if (parent is null && variableDeclaration.Kind != VariableDeclarationKind.Var)
+                        if ((parent is null or Module) && variableDeclaration.Kind != VariableDeclarationKind.Var)
                         {
                             _lexicalDeclarations ??= new List<VariableDeclaration>();
                             _lexicalDeclarations.Add(variableDeclaration);

+ 91 - 0
Jint/ModuleBuilder.cs

@@ -0,0 +1,91 @@
+#nullable enable
+
+using System;
+using System.Collections.Generic;
+using Esprima;
+using Esprima.Ast;
+using Jint.Native;
+using Jint.Runtime.Interop;
+using Jint.Runtime.Modules;
+
+namespace Jint;
+
+public sealed class ModuleBuilder
+{
+    private readonly Engine _engine;
+    private readonly List<string> _sourceRaw = new();
+    private readonly Dictionary<string, JsValue> _exports = new();
+
+    public ModuleBuilder(Engine engine)
+    {
+        _engine = engine;
+    }
+
+    public ModuleBuilder AddSource(string code)
+    {
+        _sourceRaw.Add(code);
+        return this;
+    }
+
+    public ModuleBuilder ExportValue(string name, JsValue value)
+    {
+        _exports.Add(name, value);
+        return this;
+    }
+
+    public ModuleBuilder ExportObject(string name, object value)
+    {
+        _exports.Add(name, JsValue.FromObject(_engine, value));
+        return this;
+    }
+
+    public ModuleBuilder ExportType<T>()
+    {
+        ExportType<T>(typeof(T).Name);
+        return this;
+    }
+
+    public ModuleBuilder ExportType<T>(string name)
+    {
+        _exports.Add(name, TypeReference.CreateTypeReference<T>(_engine));
+        return this;
+    }
+
+    public ModuleBuilder ExportType(Type type)
+    {
+        ExportType(type.Name, type);
+        return this;
+    }
+
+    public ModuleBuilder ExportType(string name, Type type)
+    {
+        _exports.Add(name, TypeReference.CreateTypeReference(_engine, type));
+        return this;
+    }
+
+    public ModuleBuilder ExportFunction(string name, Func<JsValue[], JsValue> fn)
+    {
+        _exports.Add(name, new ClrFunctionInstance(_engine, name, (@this, args) => fn(args)));
+        return this;
+    }
+
+    internal Module Parse()
+    {
+        if (_sourceRaw.Count > 0)
+        {
+            return new JavaScriptParser(_sourceRaw.Count == 1 ? _sourceRaw[0] : string.Join(Environment.NewLine, _sourceRaw)).ParseModule();
+        }
+        else
+        {
+            return new Module(NodeList.Create(Array.Empty<Statement>()));
+        }
+    }
+
+    internal void BindExportedValues(JsModule module)
+    {
+        foreach (var export in _exports)
+        {
+            module.BindExportedValue(export.Key, export.Value);
+        }
+    }
+}

+ 1 - 0
Jint/Native/Function/FunctionInstance.cs

@@ -348,6 +348,7 @@ namespace Jint.Native.Function
             var calleeRealm = _realm;
 
             var calleeContext = new ExecutionContext(
+                null,
                 localEnv,
                 localEnv,
                 _privateEnvironment,

+ 1 - 2
Jint/Native/Function/ScriptFunctionInstance.cs

@@ -138,8 +138,7 @@ namespace Jint.Native.Function
             {
                 try
                 {
-                    var context = _engine._activeEvaluationContext ?? new EvaluationContext(_engine);
-                    var result = OrdinaryCallEvaluateBody(context, arguments, calleeContext);
+                    var result = OrdinaryCallEvaluateBody(_engine._activeEvaluationContext, arguments, calleeContext);
 
                     // The DebugHandler needs the current execution context before the return for stepping through the return point
                     if (_engine._isDebugMode && result.Type != CompletionType.Throw)

+ 1 - 1
Jint/Native/ICallable.cs

@@ -2,6 +2,6 @@
 {
     internal interface ICallable
     {
-        JsValue Call(JsValue thisObject, JsValue[] arguments);
+        JsValue Call(JsValue thisObject, params JsValue[] arguments);
     }
 }

+ 2 - 2
Jint/Native/Object/ObjectInstance.cs

@@ -82,6 +82,8 @@ namespace Jint.Native.Object
             get => _class;
         }
 
+        public JsValue this[JsValue property] => Get(property);
+
         /// <summary>
         /// https://tc39.es/ecma262/#sec-construct
         /// </summary>
@@ -215,8 +217,6 @@ namespace Jint.Native.Object
             }
         }
 
-
-
         public virtual List<JsValue> GetOwnPropertyKeys(Types types = Types.String | Types.Symbol)
         {
             EnsureInitialized();

+ 10 - 6
Jint/Native/Promise/PromiseOperations.cs

@@ -1,3 +1,5 @@
+#nullable enable
+
 using System;
 using System.Collections.Generic;
 using Jint.Native.Object;
@@ -29,20 +31,22 @@ namespace Jint.Native.Promise
         //      j. Else,
         //          i. Let status be Call(promiseCapability.[[Resolve]], undefined, « handlerResult.[[Value]] »).
         //      k. Return Completion(status).
-        internal static Action NewPromiseReactionJob(PromiseReaction reaction, JsValue value)
+        private static Action NewPromiseReactionJob(PromiseReaction reaction, JsValue value)
         {
             return () =>
             {
+                var promiseCapability = reaction.Capability;
+
                 if (reaction.Handler is ICallable handler)
                 {
                     try
                     {
                         var result = handler.Call(JsValue.Undefined, new[] {value});
-                        reaction.Capability.Resolve.Call(JsValue.Undefined, new[] {result});
+                        promiseCapability.Resolve.Call(JsValue.Undefined, new[] {result});
                     }
                     catch (JavaScriptException e)
                     {
-                        reaction.Capability.Reject.Call(JsValue.Undefined, new[] {e.Error});
+                        promiseCapability.Reject.Call(JsValue.Undefined, new[] {e.Error});
                     }
                 }
                 else
@@ -50,13 +54,13 @@ namespace Jint.Native.Promise
                     switch (reaction.Type)
                     {
                         case ReactionType.Fulfill:
-                            reaction.Capability.Resolve.Call(JsValue.Undefined, new[] {value});
+                            promiseCapability.Resolve.Call(JsValue.Undefined, new[] {value});
                             break;
 
                         case ReactionType.Reject:
-                            reaction.Capability.Reject.Call(JsValue.Undefined, new[] {value});
-
+                            promiseCapability.Reject.Call(JsValue.Undefined, new[] {value});
                             break;
+
                         default:
                             throw new ArgumentOutOfRangeException();
                     }

+ 7 - 15
Jint/Options.Extensions.cs

@@ -238,36 +238,28 @@ namespace Jint
         /// <remarks>
         /// Passed Engine instance is still in construction and should not be used during call stage.
         /// </remarks>
-        public static void UseHostFactory<T>(this Options options, Func<Engine, T> factory) where T : Host
+        public static Options UseHostFactory<T>(this Options options, Func<Engine, T> factory) where T : Host
         {
             options.Host.Factory = factory;
+            return options;
         }
 
         /// <summary>
         /// Enables module loading in the engine via the 'require' function. By default there's no sand-boxing and
         /// you need to trust the script loading the modules not doing bad things.
         /// </summary>
-        public static Options EnableModules(this Options options, bool enable = true)
-        {
-            options.Modules.Enabled = enable;
-            return options;
-        }
-
-        /// <summary>
-        /// Allows to configure module loader implementation.
-        /// </summary>
-        public static Options WithModuleLoader<T>(this Options options) where T : IModuleLoader, new()
+        public static Options EnableModules(this Options options, string basePath, bool restrictToBasePath = true)
         {
-            return WithModuleLoader(options, new T());
+            return EnableModules(options, new DefaultModuleLoader(basePath, restrictToBasePath));
         }
 
         /// <summary>
-        /// Allows to configure module loader implementation.
+        /// Enables module loading using a custom loader implementation.
         /// </summary>
-        public static Options WithModuleLoader(this Options options, IModuleLoader moduleLoader)
+        public static Options EnableModules(this Options options, IModuleLoader moduleLoader)
         {
             options.Modules.ModuleLoader = moduleLoader;
             return options;
         }
     }
-}
+}

+ 16 - 30
Jint/Options.cs

@@ -103,31 +103,22 @@ namespace Jint
                 AttachExtensionMethodsToPrototypes(engine);
             }
 
-            var moduleLoader = Modules.ModuleLoader;
-            if (Modules.Enabled)
+            if (Modules.RegisterRequire)
             {
-                if (ReferenceEquals(moduleLoader, FailFastModuleLoader.Instance))
-                {
-                    moduleLoader = new DefaultModuleLoader(new System.IO.FileInfo(Assembly.GetEntryAssembly().CodeBase).DirectoryName);
-                }
-
-                if (Modules.RegisterRequire)
-                {
-                    // Node js like loading of modules
-                    engine.Realm.GlobalObject.SetProperty("require", new PropertyDescriptor(new ClrFunctionInstance(
-                            engine,
-                            "require",
-                            (thisObj, arguments) =>
-                            {
-                                var specifier = TypeConverter.ToString(arguments.At(0));
-                                var module = engine.LoadModule(specifier);
-                                return JsModule.GetModuleNamespace(module);
-                            }),
-                        PropertyFlag.AllForbidden));
-                }
+                // Node js like loading of modules
+                engine.Realm.GlobalObject.SetProperty("require", new PropertyDescriptor(new ClrFunctionInstance(
+                        engine,
+                        "require",
+                        (thisObj, arguments) =>
+                        {
+                            var specifier = TypeConverter.ToString(arguments.At(0));
+                            var module = engine.LoadModule(specifier);
+                            return JsModule.GetModuleNamespace(module);
+                        }),
+                    PropertyFlag.AllForbidden));
             }
 
-            engine.ModuleLoader = moduleLoader;
+            engine.ModuleLoader = Modules.ModuleLoader;
 
             // ensure defaults
             engine.ClrTypeConverter ??= new DefaultTypeConverter(engine);
@@ -357,15 +348,10 @@ namespace Jint
     }
 
     /// <summary>
-    /// Module related customization, work in progress
+    /// Module related customization
     /// </summary>
     public class ModuleOptions
     {
-        /// <summary>
-        /// Indicates if modules are enabled in the current engine context, defaults to false.
-        /// </summary>
-        public bool Enabled { get; set; }
-
         /// <summary>
         /// Whether to register require function to engine which will delegate to module loader, defaults to false.
         /// </summary>
@@ -374,6 +360,6 @@ namespace Jint
         /// <summary>
         /// Module loader implementation, by default exception will be thrown if module loading is not enabled.
         /// </summary>
-        public IModuleLoader? ModuleLoader { get; set; } = FailFastModuleLoader.Instance;
+        public IModuleLoader ModuleLoader { get; set; } = FailFastModuleLoader.Instance;
     }
-}
+}

+ 5 - 3
Jint/Runtime/Environments/ExecutionContext.cs

@@ -7,12 +7,14 @@ namespace Jint.Runtime.Environments
     internal readonly struct ExecutionContext
     {
         internal ExecutionContext(
+            IScriptOrModule? scriptOrModule,
             EnvironmentRecord lexicalEnvironment,
             EnvironmentRecord variableEnvironment,
             PrivateEnvironmentRecord? privateEnvironment,
             Realm realm,
             FunctionInstance? function = null)
         {
+            ScriptOrModule = scriptOrModule;
             LexicalEnvironment = lexicalEnvironment;
             VariableEnvironment = variableEnvironment;
             PrivateEnvironment = privateEnvironment;
@@ -20,8 +22,8 @@ namespace Jint.Runtime.Environments
             Function = function;
         }
 
+        public readonly IScriptOrModule? ScriptOrModule;
         public readonly EnvironmentRecord LexicalEnvironment;
-
         public readonly EnvironmentRecord VariableEnvironment;
         public readonly PrivateEnvironmentRecord? PrivateEnvironment;
         public readonly Realm Realm;
@@ -29,12 +31,12 @@ namespace Jint.Runtime.Environments
 
         public ExecutionContext UpdateLexicalEnvironment(EnvironmentRecord lexicalEnvironment)
         {
-            return new ExecutionContext(lexicalEnvironment, VariableEnvironment, PrivateEnvironment, Realm, Function);
+            return new ExecutionContext(ScriptOrModule, lexicalEnvironment, VariableEnvironment, PrivateEnvironment, Realm, Function);
         }
 
         public ExecutionContext UpdateVariableEnvironment(EnvironmentRecord variableEnvironment)
         {
-            return new ExecutionContext(LexicalEnvironment, variableEnvironment, PrivateEnvironment, Realm, Function);
+            return new ExecutionContext(ScriptOrModule, LexicalEnvironment, variableEnvironment, PrivateEnvironment, Realm, Function);
         }
 
         /// <summary>

+ 1 - 1
Jint/Runtime/Environments/FunctionEnvironmentRecord.cs

@@ -345,7 +345,7 @@ namespace Jint.Runtime.Environments
                 var oldEnv = _engine.ExecutionContext.LexicalEnvironment;
                 var paramVarEnv = JintEnvironment.NewDeclarativeEnvironment(_engine, oldEnv);
 
-                _engine.EnterExecutionContext(new ExecutionContext(paramVarEnv, paramVarEnv, null, _engine.Realm, null));
+                _engine.EnterExecutionContext(new ExecutionContext(null, paramVarEnv, paramVarEnv, null, _engine.Realm, null));
                 try
                 {
                     argument = jintExpression.GetValue(context).Value;

+ 8 - 7
Jint/Runtime/Environments/ModuleEnvironmentRecord.cs

@@ -27,26 +27,27 @@ internal sealed class ModuleEnvironmentRecord : DeclarativeEnvironmentRecord
         _importBindings[importName] = new IndirectBinding(module, name);
     }
 
+    // https://tc39.es/ecma262/#sec-module-environment-records-getbindingvalue-n-s
     public override JsValue GetBindingValue(string name, bool strict)
     {
         if (_importBindings.TryGetValue(name, out var indirectBinding))
         {
-            return base.GetBindingValue(name, strict);
+            return indirectBinding.Module._environment.GetBindingValue(indirectBinding.BindingName, true);
         }
 
-        return indirectBinding.Module._environment.GetBindingValue(indirectBinding.BindingName, true);
+        return base.GetBindingValue(name, strict);
     }
 
     internal override bool TryGetBinding(in BindingName name, bool strict, out Binding binding, out JsValue value)
     {
-        if (!_importBindings.TryGetValue(name.Key, out var indirectBinding))
+        if (_importBindings.TryGetValue(name.Key, out var indirectBinding))
         {
-            return base.TryGetBinding(name, strict, out binding, out value);
+            value = indirectBinding.Module._environment.GetBindingValue(indirectBinding.BindingName, true);
+            binding = new(value, canBeDeleted: false, mutable: false, strict: true);
+            return true;
         }
 
-        value = indirectBinding.Module._environment.GetBindingValue(indirectBinding.BindingName, true);
-        binding = new(value, false, false, true);
-        return true;
+        return base.TryGetBinding(name, strict, out binding, out value);
     }
 
     public override bool HasThisBinding() => true;

+ 15 - 1
Jint/Runtime/ExceptionHelper.cs

@@ -2,8 +2,10 @@
 using System.Diagnostics.CodeAnalysis;
 using System.Reflection;
 using System.Runtime.ExceptionServices;
+using Esprima;
 using Jint.Native;
 using Jint.Runtime.CallStack;
+using Jint.Runtime.Modules;
 using Jint.Runtime.References;
 
 namespace Jint.Runtime
@@ -16,6 +18,12 @@ namespace Jint.Runtime
             throw new JavaScriptException(realm.Intrinsics.SyntaxError, message);
         }
 
+        [DoesNotReturn]
+        public static void ThrowSyntaxError(Realm realm, string message, Location location)
+        {
+            throw new JavaScriptException(realm.Intrinsics.SyntaxError, message).SetLocation(location);
+        }
+
         [DoesNotReturn]
         public static void ThrowArgumentException(string message = null)
         {
@@ -168,5 +176,11 @@ namespace Jint.Runtime
         {
             throw new ExecutionCanceledException();
         }
+
+        [DoesNotReturn]
+        public static void ThrowModuleResolutionException(string resolverAlgorithmError, string specifier, string parent)
+        {
+            throw new ModuleResolutionException(resolverAlgorithmError, specifier, parent);
+        }
     }
-}
+}

+ 17 - 1
Jint/Runtime/ExecutionContextStack.cs

@@ -37,5 +37,21 @@ namespace Jint.Runtime
 
         [MethodImpl(MethodImplOptions.AggressiveInlining)]
         public ref readonly ExecutionContext Pop() => ref _stack.Pop();
+
+        public IScriptOrModule? GetActiveScriptOrModule()
+        {
+            var array = _stack._array;
+            var size = _stack._size;
+            for (var i = size - 1; i > -1; --i)
+            {
+                var context = array[i];
+                if (context.ScriptOrModule is not null)
+                {
+                    return context.ScriptOrModule;
+                }
+            }
+
+            return null;
+        }
     }
-}
+}

+ 4 - 3
Jint/Runtime/Host.cs

@@ -35,6 +35,7 @@ namespace Jint.Runtime
             var realm = CreateRealm();
 
             var newContext = new ExecutionContext(
+                scriptOrModule: null,
                 lexicalEnvironment: realm.GlobalEnv,
                 variableEnvironment: realm.GlobalEnv,
                 privateEnvironment: null,
@@ -106,7 +107,7 @@ namespace Jint.Runtime
         /// <returns></returns>
         protected internal virtual JsModule ResolveImportedModule(JsModule referencingModule, string specifier)
         {
-            return Engine.LoadModule(referencingModule._location, specifier);
+            return Engine.LoadModule(referencingModule.Location, specifier);
         }
 
         /// <summary>
@@ -121,7 +122,7 @@ namespace Jint.Runtime
 
             try
             {
-                Engine.LoadModule(referencingModule._location, specifier);
+                Engine.LoadModule(referencingModule.Location, specifier);
                 promise.Resolve(JsValue.Undefined);
 
             }
@@ -164,7 +165,7 @@ namespace Jint.Runtime
                 return JsValue.Undefined;
             }, 0, PropertyFlag.Configurable);
 
-            PromiseOperations.PerformPromiseThen(Engine, innerPromise, onFulfilled, onRejected, null);
+            PromiseOperations.PerformPromiseThen(Engine, innerPromise, onFulfilled, onRejected, promiseCapability);
         }
     }
 }

+ 20 - 0
Jint/Runtime/IScriptOrModule.Extensions.cs

@@ -0,0 +1,20 @@
+#nullable enable
+
+using Esprima;
+using Jint.Runtime.Modules;
+
+namespace Jint.Runtime;
+
+internal static class ScriptOrModuleExtensions
+{
+    public static JsModule AsModule(this IScriptOrModule? scriptOrModule, Engine engine, Location location)
+    {
+        var module = scriptOrModule as JsModule;
+        if (module == null)
+        {
+            ExceptionHelper.ThrowSyntaxError(engine.Realm, "Cannot use import/export statements outside a module", location);
+            return default!;
+        }
+        return module;
+    }
+}

+ 5 - 0
Jint/Runtime/IScriptOrModule.cs

@@ -0,0 +1,5 @@
+namespace Jint.Runtime;
+
+internal interface IScriptOrModule
+{
+}

+ 5 - 3
Jint/Runtime/Interpreter/JintStatementList.cs

@@ -42,11 +42,13 @@ namespace Jint.Runtime.Interpreter
             for (var i = 0; i < jintStatements.Length; i++)
             {
                 var esprimaStatement = _statements[i];
+                var statement = JintStatement.Build(esprimaStatement);
+                // When in debug mode, don't do FastResolve: Stepping requires each statement to be actually executed.
+                var value = context.DebugMode ? null : JintStatement.FastResolve(esprimaStatement);
                 jintStatements[i] = new Pair
                 {
-                    Statement = JintStatement.Build(esprimaStatement),
-                    // When in debug mode, don't do FastResolve: Stepping requires each statement to be actually executed.
-                    Value = context.DebugMode ? null : JintStatement.FastResolve(esprimaStatement)
+                    Statement = statement,
+                    Value = value
                 };
             }
             _jintStatements = jintStatements;

+ 21 - 0
Jint/Runtime/Interpreter/Statements/JintExportAllDeclaration.cs

@@ -0,0 +1,21 @@
+#nullable enable
+
+using Esprima.Ast;
+
+namespace Jint.Runtime.Interpreter.Statements;
+
+internal sealed class JintExportAllDeclaration : JintStatement<ExportAllDeclaration>
+{
+    public JintExportAllDeclaration(ExportAllDeclaration statement) : base(statement)
+    {
+    }
+
+    protected override void Initialize(EvaluationContext context)
+    {
+    }
+
+    protected override Completion ExecuteInternal(EvaluationContext context)
+    {
+        return Completion.Empty();
+    }
+}

+ 31 - 0
Jint/Runtime/Interpreter/Statements/JintExportDefaultDeclaration.cs

@@ -0,0 +1,31 @@
+#nullable enable
+
+using Esprima.Ast;
+using Jint.Runtime.Interpreter.Expressions;
+
+namespace Jint.Runtime.Interpreter.Statements;
+
+internal sealed class JintExportDefaultDeclaration : JintStatement<ExportDefaultDeclaration>
+{
+    private JintExpression? _init;
+
+    public JintExportDefaultDeclaration(ExportDefaultDeclaration statement) : base(statement)
+    {
+    }
+
+    protected override void Initialize(EvaluationContext context)
+    {
+        _init = JintExpression.Build(context.Engine, (Expression)_statement.Declaration);
+    }
+
+    // https://tc39.es/ecma262/#sec-exports-runtime-semantics-evaluation
+    protected override Completion ExecuteInternal(EvaluationContext context)
+    {
+        var module = context.Engine.GetActiveScriptOrModule().AsModule(context.Engine, context.LastSyntaxNode.Location);
+
+        var completion = _init?.GetValue(context) ?? Completion.Empty();
+        module._environment.CreateImmutableBindingAndInitialize("*default*", true, completion.Value);
+
+        return Completion.Empty();
+    }
+}

+ 98 - 0
Jint/Runtime/Interpreter/Statements/JintExportNamedDeclaration.cs

@@ -0,0 +1,98 @@
+#nullable enable
+
+using Esprima.Ast;
+using Jint.Native;
+using Jint.Runtime.Interpreter.Expressions;
+
+namespace Jint.Runtime.Interpreter.Statements;
+
+internal sealed class JintExportNamedDeclaration : JintStatement<ExportNamedDeclaration>
+{
+    private JintExpression? _declarationExpression;
+    private JintStatement? _declarationStatement;
+    private ExportedSpecifier[]? _specifiers;
+
+    private sealed record ExportedSpecifier(
+        JintExpression Local,
+        JintExpression Exported
+    );
+
+    public JintExportNamedDeclaration(ExportNamedDeclaration statement) : base(statement)
+    {
+    }
+
+    protected override void Initialize(EvaluationContext context)
+    {
+        if (_statement.Declaration != null)
+        {
+            switch (_statement.Declaration)
+            {
+                case Expression e:
+                    _declarationExpression = JintExpression.Build(context.Engine, e);
+                    break;
+                case Statement s:
+                    _declarationStatement = Build(s);
+                    break;
+                default:
+                    ExceptionHelper.ThrowNotSupportedException($"Statement {_statement.Declaration.Type} is not supported in an export declaration.");
+                    break;
+            }
+        }
+
+        if (_statement.Specifiers.Count > 0)
+        {
+            _specifiers = new ExportedSpecifier[_statement.Specifiers.Count];
+            ref readonly var statementSpecifiers = ref _statement.Specifiers;
+            for (var i = 0; i < statementSpecifiers.Count; i++)
+            {
+                var statementSpecifier = statementSpecifiers[i];
+
+                _specifiers[i] = new ExportedSpecifier(
+                    Local: JintExpression.Build(context.Engine, statementSpecifier.Local),
+                    Exported: JintExpression.Build(context.Engine, statementSpecifier.Exported)
+                );
+            }
+        }
+    }
+
+    /// <summary>
+    /// https://tc39.es/ecma262/#sec-exports-runtime-semantics-evaluation
+    /// </summary>
+    protected override Completion ExecuteInternal(EvaluationContext context)
+    {
+        var module = context.Engine.GetActiveScriptOrModule().AsModule(context.Engine, context.LastSyntaxNode.Location);
+
+        if (_specifiers != null)
+        {
+            foreach (var specifier in _specifiers)
+            {
+                if (specifier.Local is not JintIdentifierExpression local || specifier.Exported is not JintIdentifierExpression exported)
+                {
+                    ExceptionHelper.ThrowSyntaxError(context.Engine.Realm, "", context.LastSyntaxNode.Location);
+                    return default;
+                }
+
+                var localKey = local._expressionName.Key.Name;
+                var exportedKey = exported._expressionName.Key.Name;
+                if (localKey != exportedKey)
+                {
+                    module._environment.CreateImportBinding(exportedKey, module, localKey);
+                }
+            }
+        }
+
+        if (_declarationStatement != null)
+        {
+            _declarationStatement.Execute(context);
+            return NormalCompletion(Undefined.Instance);
+        }
+
+        if (_declarationExpression != null)
+        {
+            // Named exports don't require anything more since the values are available in the lexical context
+            return _declarationExpression.GetValue(context);
+        }
+
+        return NormalCompletion(Undefined.Instance);
+    }
+}

+ 29 - 0
Jint/Runtime/Interpreter/Statements/JintImportDeclaration.cs

@@ -0,0 +1,29 @@
+#nullable enable
+
+using Esprima.Ast;
+using Jint.Native.Promise;
+
+namespace Jint.Runtime.Interpreter.Statements;
+
+internal sealed class JintImportDeclaration : JintStatement<ImportDeclaration>
+{
+    public JintImportDeclaration(ImportDeclaration statement) : base(statement)
+    {
+    }
+
+    protected override void Initialize(EvaluationContext context)
+    {
+    }
+
+    protected override Completion ExecuteInternal(EvaluationContext context)
+    {
+        var module = context.Engine.GetActiveScriptOrModule().AsModule(context.Engine, context.LastSyntaxNode.Location);
+        var specifier = _statement.Source.StringValue;
+        var promiseCapability = PromiseConstructor.NewPromiseCapability(context.Engine, context.Engine.Realm.Intrinsics.Promise);
+        var specifierString = TypeConverter.ToString(specifier);
+
+        // TODO: This comment was in @lahma's code: 6.IfAbruptRejectPromise(specifierString, promiseCapability);
+        context.Engine._host.ImportModuleDynamically(module, specifierString, promiseCapability);
+        return NormalCompletion(promiseCapability.PromiseInstance);
+    }
+}

+ 4 - 4
Jint/Runtime/Interpreter/Statements/JintStatement.cs

@@ -90,10 +90,10 @@ namespace Jint.Runtime.Interpreter.Statements
                 Nodes.DebuggerStatement => new JintDebuggerStatement((DebuggerStatement) statement),
                 Nodes.Program when statement is Script s => new JintScript(s),
                 Nodes.ClassDeclaration => new JintClassDeclarationStatement((ClassDeclaration) statement),
-                Nodes.ExportAllDeclaration or
-                Nodes.ExportDefaultDeclaration or
-                Nodes.ExportNamedDeclaration or
-                Nodes.ImportDeclaration => new JintEmptyStatement(new EmptyStatement()),
+                Nodes.ExportNamedDeclaration => new JintExportNamedDeclaration((ExportNamedDeclaration) statement),
+                Nodes.ExportAllDeclaration => new JintExportAllDeclaration((ExportAllDeclaration) statement),
+                Nodes.ExportDefaultDeclaration => new JintExportDefaultDeclaration((ExportDefaultDeclaration) statement),
+                Nodes.ImportDeclaration => new JintImportDeclaration((ImportDeclaration) statement),
                 _ => null
             };
 

+ 7 - 0
Jint/Runtime/JavaScriptException.cs

@@ -35,6 +35,13 @@ namespace Jint.Runtime
             Error = error;
         }
 
+        internal JavaScriptException SetLocation(Location location)
+        {
+            Location = location;
+
+            return this;
+        }
+
         internal JavaScriptException SetCallstack(Engine engine, Location location)
         {
             Location = location;

+ 111 - 26
Jint/Runtime/Modules/DefaultModuleLoader.cs

@@ -7,42 +7,132 @@ using Esprima.Ast;
 
 namespace Jint.Runtime.Modules;
 
-public class DefaultModuleLoader : IModuleLoader
+public sealed class DefaultModuleLoader : IModuleLoader
 {
-    private readonly string _basePath;
+    private readonly Uri _basePath;
+    private readonly bool _restrictToBasePath;
 
-    public DefaultModuleLoader(string basePath)
+    public DefaultModuleLoader(string basePath) : this(basePath, true)
     {
-        _basePath = basePath;
+
     }
 
-    public virtual ModuleLoaderResult LoadModule(Engine engine, string location, string? referencingLocation)
+    public DefaultModuleLoader(string basePath, bool restrictToBasePath)
     {
-        // If no referencing location is provided, ensure location is absolute
+        if (string.IsNullOrWhiteSpace(basePath))
+        {
+            ExceptionHelper.ThrowArgumentException("Value cannot be null or whitespace.", nameof(basePath));
+        }
+
+        _restrictToBasePath = restrictToBasePath;
+
+        if (!Uri.TryCreate(basePath, UriKind.Absolute, out _basePath))
+        {
+            if (!Path.IsPathRooted(basePath))
+            {
+                ExceptionHelper.ThrowArgumentException("Path must be rooted", nameof(basePath));
+            }
+
+            basePath = Path.GetFullPath(basePath);
+            _basePath = new Uri(basePath, UriKind.Absolute);
+        }
+
+        if (_basePath.AbsolutePath[_basePath.AbsolutePath.Length - 1] != '/')
+        {
+            var uriBuilder = new UriBuilder(_basePath);
+            uriBuilder.Path += '/';
+            _basePath = uriBuilder.Uri;
+        }
+    }
 
-        var locationUri = referencingLocation == null 
-            ? new Uri(location, UriKind.Absolute) 
-            : new Uri(new Uri(referencingLocation, UriKind.Absolute), location)
-            ;
+    public ResolvedSpecifier Resolve(string? referencingModuleLocation, string specifier)
+    {
+        if (string.IsNullOrEmpty(specifier))
+        {
+            ExceptionHelper.ThrowModuleResolutionException("Invalid Module Specifier", specifier, referencingModuleLocation);
+            return default;
+        }
 
-        // Ensure the resulting resource is under the base path if it is provided
+        // Specifications from ESM_RESOLVE Algorithm: https://nodejs.org/api/esm.html#resolution-algorithm
 
-        if (!String.IsNullOrEmpty(_basePath) && !locationUri.AbsolutePath.StartsWith(_basePath, StringComparison.Ordinal))
+        Uri resolved;
+        if (Uri.TryCreate(specifier, UriKind.Absolute, out var uri))
+        {
+            resolved = uri;
+        }
+        else if (IsRelative(specifier))
+        {
+            resolved = new Uri(referencingModuleLocation != null ? new Uri(referencingModuleLocation, UriKind.Absolute) : _basePath, specifier);
+        }
+        else if (specifier[0] == '#')
+        {
+            ExceptionHelper.ThrowNotSupportedException($"PACKAGE_IMPORTS_RESOLVE is not supported: '{specifier}'");
+            return default;
+        }
+        else
         {
-            ExceptionHelper.ThrowArgumentException("Invalid file location.");
+            return new ResolvedSpecifier(
+                specifier,
+                specifier,
+                null,
+                SpecifierType.Bare
+            );
         }
 
-        return LoadModule(engine, locationUri);
+        if (resolved.IsFile)
+        {
+            if (resolved.UserEscaped)
+            {
+                ExceptionHelper.ThrowModuleResolutionException("Invalid Module Specifier", specifier, referencingModuleLocation);
+                return default;
+            }
+
+            if (!Path.HasExtension(resolved.LocalPath))
+            {
+                ExceptionHelper.ThrowModuleResolutionException("Unsupported Directory Import", specifier, referencingModuleLocation);
+                return default;
+            }
+        }
+
+        if (_restrictToBasePath && !_basePath.IsBaseOf(resolved))
+        {
+            ExceptionHelper.ThrowModuleResolutionException($"Unauthorized Module Path", specifier, referencingModuleLocation);
+            return default;
+        }
+
+        return new ResolvedSpecifier(
+            specifier,
+            resolved.AbsoluteUri,
+            resolved,
+            SpecifierType.RelativeOrAbsolute
+        );
     }
 
-    protected virtual ModuleLoaderResult LoadModule(Engine engine, Uri location)
+    public Module LoadModule(Engine engine, ResolvedSpecifier resolved)
     {
-        var code = LoadModuleSourceCode(location);
+        if (resolved.Type != SpecifierType.RelativeOrAbsolute)
+        {
+            ExceptionHelper.ThrowNotSupportedException($"The default module loader can only resolve files. You can define modules directly to allow imports using {nameof(Engine)}.{nameof(Engine.AddModule)}(). Attempted to resolve: '{resolved.Specifier}'.");
+            return default;
+        }
+
+        if (resolved.Uri == null)
+        {
+            ExceptionHelper.ThrowInvalidOperationException($"Module '{resolved.Specifier}' of type '{resolved.Type}' has no resolved URI.");
+        }
+
+        if (!File.Exists(resolved.Uri.AbsolutePath))
+        {
+            ExceptionHelper.ThrowArgumentException("Module Not Found: ", resolved.Specifier);
+            return default;
+        }
+
+        var code = File.ReadAllText(resolved.Uri.LocalPath);
 
         Module module;
         try
         {
-            var parserOptions = new ParserOptions(location.ToString())
+            var parserOptions = new ParserOptions(resolved.Uri.LocalPath)
             {
                 AdaptRegexp = true,
                 Tolerant = true
@@ -52,20 +142,15 @@ public class DefaultModuleLoader : IModuleLoader
         }
         catch (ParserException ex)
         {
-            ExceptionHelper.ThrowSyntaxError(engine.Realm, $"Error while loading module: error in module '{location}': {ex.Error}");
+            ExceptionHelper.ThrowSyntaxError(engine.Realm, $"Error while loading module: error in module '{resolved.Uri.LocalPath}': {ex.Error}");
             module = null;
         }
 
-        return new ModuleLoaderResult(module, location);
+        return module;
     }
 
-    protected virtual string LoadModuleSourceCode(Uri location)
+    private static bool IsRelative(string specifier)
     {
-        if (!location.IsFile)
-        {
-            ExceptionHelper.ThrowArgumentException("Only file loading is supported");
-        }
-
-        return File.ReadAllText(location.AbsolutePath);
+        return specifier.StartsWith(".") || specifier.StartsWith("/");
     }
 }

+ 19 - 2
Jint/Runtime/Modules/FailFastModuleLoader.cs

@@ -1,15 +1,32 @@
 #nullable enable
 
+using System;
+using Esprima.Ast;
+
 namespace Jint.Runtime.Modules;
 
 internal sealed class FailFastModuleLoader : IModuleLoader
 {
     public static readonly IModuleLoader Instance = new FailFastModuleLoader();
 
-    public ModuleLoaderResult LoadModule(Engine engine, string location, string? referencingLocation)
+    public Uri BasePath
+    {
+        get
+        {
+            ExceptionHelper.ThrowInvalidOperationException("Cannot access base path when modules loading is disabled");
+            return default;
+        }
+    }
+
+    public ResolvedSpecifier Resolve(string? referencingModuleLocation, string specifier)
+    {
+        return new ResolvedSpecifier(specifier, specifier, null, SpecifierType.Bare);
+    }
+
+    public Module LoadModule(Engine engine, ResolvedSpecifier resolved)
     {
         ThrowDisabledException();
-        return default;
+        return default!;
     }
 
     private static void ThrowDisabledException()

+ 7 - 8
Jint/Runtime/Modules/IModuleLoader.cs

@@ -1,22 +1,21 @@
 #nullable enable
 
-using System;
 using Esprima.Ast;
 
 namespace Jint.Runtime.Modules;
 
-/// <summary>
-/// Module loading result.
-/// </summary>
-public readonly record struct ModuleLoaderResult(Module Module, Uri Location);
-
 /// <summary>
 /// Module loader interface that allows defining how module loadings requests are handled.
 /// </summary>
 public interface IModuleLoader
 {
+    /// <summary>
+    /// Resolves a specifier to a path or module
+    /// </summary>
+    ResolvedSpecifier Resolve(string? referencingModuleLocation, string specifier);
+
     /// <summary>
     /// Loads a module from given location.
     /// </summary>
-    public ModuleLoaderResult LoadModule(Engine engine, string location, string? referencingLocation);
-}
+    public Module LoadModule(Engine engine, ResolvedSpecifier resolved);
+}

+ 83 - 45
Jint/Runtime/Modules/JsModule.cs

@@ -2,6 +2,7 @@
 using Esprima.Ast;
 using System.Collections.Generic;
 using System.Linq;
+using Esprima;
 using Jint.Native;
 using Jint.Native.Object;
 using Jint.Native.Promise;
@@ -43,7 +44,7 @@ internal sealed record ExportResolveSetItem(
 /// https://tc39.es/ecma262/#sec-cyclic-module-records
 /// https://tc39.es/ecma262/#sec-source-text-module-records
 /// </summary>
-public sealed class JsModule : JsValue
+public sealed class JsModule : JsValue, IScriptOrModule
 {
     private readonly Engine _engine;
     private readonly Realm _realm;
@@ -68,7 +69,6 @@ public sealed class JsModule : JsValue
     private readonly List<ExportEntry> _localExportEntries;
     private readonly List<ExportEntry> _indirectExportEntries;
     private readonly List<ExportEntry> _starExportEntries;
-    internal readonly string _location;
     internal JsValue _evalResult;
 
     internal JsModule(Engine engine, Realm realm, Module source, string location, bool async) : base(InternalTypes.Module)
@@ -76,7 +76,7 @@ public sealed class JsModule : JsValue
         _engine = engine;
         _realm = realm;
         _source = source;
-        _location = location;
+        Location = location;
 
         _importMeta = _realm.Intrinsics.Object.Construct(1);
         _importMeta.DefineOwnProperty("url", new PropertyDescriptor(location, PropertyFlag.ConfigurableEnumerableWritable));
@@ -93,6 +93,7 @@ public sealed class JsModule : JsValue
 
     }
 
+    public string Location { get; }
     internal ModuleStatus Status { get; private set; }
 
     /// <summary>
@@ -131,6 +132,12 @@ public sealed class JsModule : JsValue
         return m;
     }
 
+    internal void BindExportedValue(string name, JsValue value)
+    {
+        _environment.CreateImmutableBindingAndInitialize(name, true, value);
+        _localExportEntries.Add(new ExportEntry(name, null, null, null));
+    }
+
     /// <summary>
     /// https://tc39.es/ecma262/#sec-getexportednames
     /// </summary>
@@ -148,13 +155,13 @@ public sealed class JsModule : JsValue
         for (var i = 0; i < _localExportEntries.Count; i++)
         {
             var e = _localExportEntries[i];
-            exportedNames.Add(e.ExportName);
+            exportedNames.Add(e.ImportName ?? e.ExportName);
         }
 
         for (var i = 0; i < _indirectExportEntries.Count; i++)
         {
             var e = _indirectExportEntries[i];
-            exportedNames.Add(e.ExportName);
+            exportedNames.Add(e.ImportName ?? e.ExportName);
         }
 
         for(var i = 0; i < _starExportEntries.Count; i++)
@@ -165,7 +172,7 @@ public sealed class JsModule : JsValue
 
             for (var j = 0; j < starNames.Count; j++)
             {
-                var n = starNames[i];
+                var n = starNames[j];
                 if (!"default".Equals(n) && !exportedNames.Contains(n))
                 {
                     exportedNames.Add(n);
@@ -198,16 +205,16 @@ public sealed class JsModule : JsValue
         {
             var e = _localExportEntries[i];
 
-            if (exportName == e.ExportName)
+            if (exportName == (e.ImportName ?? e.ExportName))
             {
-                return new ResolvedBinding(this, e.LocalName);
+                return new ResolvedBinding(this, e.LocalName ?? e.ExportName);
             }
         }
 
         for(var i = 0; i < _indirectExportEntries.Count; i++)
         {
             var e = _localExportEntries[i];
-            if (exportName.Equals(e.ExportName))
+            if (exportName.Equals(e.ImportName ?? e.ExportName))
             {
                 var importedModule = _engine._host.ResolveImportedModule(this, e.ModuleRequest);
                 if(e.ImportName == "*")
@@ -374,7 +381,8 @@ public sealed class JsModule : JsValue
     /// </summary>
     private int Link(JsModule module, Stack<JsModule> stack, int index)
     {
-        if(module.Status is ModuleStatus.Linking or
+        if(module.Status is
+           ModuleStatus.Linking or
            ModuleStatus.Linked or
            ModuleStatus.EvaluatingAsync or
            ModuleStatus.Evaluating)
@@ -399,16 +407,20 @@ public sealed class JsModule : JsValue
         {
             var requiredModule = _engine._host.ResolveImportedModule(module, moduleSpecifier);
 
+            //TODO: Should we link only when a module is requested? https://tc39.es/ecma262/#sec-example-cyclic-module-record-graphs Should we support retry?
+            if (requiredModule.Status == ModuleStatus.Unlinked)
+                requiredModule.Link();
+
             if (requiredModule.Status != ModuleStatus.Linking &&
                 requiredModule.Status != ModuleStatus.Linked &&
                 requiredModule.Status != ModuleStatus.Evaluated)
             {
-                ExceptionHelper.ThrowInvalidOperationException("Error while linking module: Required module is in an invalid state");
+                ExceptionHelper.ThrowInvalidOperationException($"Error while linking module: Required module is in an invalid state: {requiredModule.Status}");
             }
 
             if(requiredModule.Status == ModuleStatus.Linking && !stack.Contains(requiredModule))
             {
-                ExceptionHelper.ThrowInvalidOperationException("Error while linking module: Required module is in an invalid state");
+                ExceptionHelper.ThrowInvalidOperationException($"Error while linking module: Required module is in an invalid state: {requiredModule.Status}");
             }
 
             if (requiredModule.Status == ModuleStatus.Linking)
@@ -492,16 +504,38 @@ public sealed class JsModule : JsValue
 
             index = TypeConverter.ToInt32(result.Value);
 
+            // TODO: Validate this behavior: https://tc39.es/ecma262/#sec-example-cyclic-module-record-graphs
+            if (requiredModule.Status == ModuleStatus.Linked)
+            {
+                var evaluationResult = requiredModule.Evaluate();
+                if (evaluationResult == null)
+                {
+                    ExceptionHelper.ThrowInvalidOperationException($"Error while evaluating module: Module evaluation did not return a promise");
+                }
+                else if (evaluationResult is not PromiseInstance promise)
+                {
+                    ExceptionHelper.ThrowInvalidOperationException($"Error while evaluating module: Module evaluation did not return a promise: {evaluationResult.Type}");
+                }
+                else if (promise.State == PromiseState.Rejected)
+                {
+                    ExceptionHelper.ThrowJavaScriptException(_engine, promise.Value, new Completion(CompletionType.Throw, promise.Value, null, new Location(new Position(), new Position(), moduleSpecifier)));
+                }
+                else if (promise.State != PromiseState.Fulfilled)
+                {
+                    ExceptionHelper.ThrowInvalidOperationException($"Error while evaluating module: Module evaluation did not return a fulfilled promise: {promise.State}");
+                }
+            }
+
             if (requiredModule.Status != ModuleStatus.Evaluating &&
                 requiredModule.Status != ModuleStatus.EvaluatingAsync &&
                 requiredModule.Status != ModuleStatus.Evaluated)
             {
-                ExceptionHelper.ThrowInvalidOperationException("Error while evaluating module: Module is in an invalid state");
+                ExceptionHelper.ThrowInvalidOperationException($"Error while evaluating module: Module is in an invalid state: {requiredModule.Status}");
             }
 
             if (requiredModule.Status == ModuleStatus.Evaluating && !stack.Contains(requiredModule))
             {
-                ExceptionHelper.ThrowInvalidOperationException("Error while evaluating module: Module is in an invalid state");
+                ExceptionHelper.ThrowInvalidOperationException($"Error while evaluating module: Module is in an invalid state: {requiredModule.Status}");
             }
 
             if(requiredModule.Status == ModuleStatus.Evaluating)
@@ -524,6 +558,8 @@ public sealed class JsModule : JsValue
             }
         }
 
+        Completion completion;
+
         if(module._pendingAsyncDependencies > 0 || module._hasTLA)
         {
             if (module._asyncEvaluation)
@@ -535,16 +571,16 @@ public sealed class JsModule : JsValue
             module._asyncEvalOrder = asyncEvalOrder++;
             if (module._pendingAsyncDependencies == 0)
             {
-                module.ExecuteAsync();
+                completion = module.ExecuteAsync();
             }
             else
             {
-                module.Execute();
+                completion = module.Execute();
             }
         }
         else
         {
-            module.Execute();
+            completion = module.Execute();
         }
 
         if(stack.Count(x => x == module) != 1)
@@ -577,8 +613,7 @@ public sealed class JsModule : JsValue
             }
         }
 
-        return new Completion(CompletionType.Normal, index, null, default);
-
+        return completion;
     }
 
     /// <summary>
@@ -600,38 +635,41 @@ public sealed class JsModule : JsValue
         var env = JintEnvironment.NewModuleEnvironment(_engine, realm.GlobalEnv);
         _environment = env;
 
-        for (var i = 0; i < _importEntries.Count; i++)
+        if (_importEntries != null)
         {
-            var ie = _importEntries[i];
-            var importedModule = _engine._host.ResolveImportedModule(this, ie.ModuleRequest);
-            if(ie.ImportName == "*")
+            for (var i = 0; i < _importEntries.Count; i++)
             {
-                var ns = GetModuleNamespace(importedModule);
-                env.CreateImmutableBinding(ie.LocalName, true);
-                env.InitializeBinding(ie.LocalName, ns);
-            }
-            else
-            {
-                var resolution = importedModule.ResolveExport(ie.ImportName);
-                if(resolution is null || resolution == ResolvedBinding.Ambiguous)
-                {
-                    ExceptionHelper.ThrowSyntaxError(_realm, "Ambigous import statement for identifier " + ie.ImportName);
-                }
-
-                if (resolution.BindingName == "*namespace*")
+                var ie = _importEntries[i];
+                var importedModule = _engine._host.ResolveImportedModule(this, ie.ModuleRequest);
+                if (ie.ImportName == "*")
                 {
-                    var ns = GetModuleNamespace(resolution.Module);
+                    var ns = GetModuleNamespace(importedModule);
                     env.CreateImmutableBinding(ie.LocalName, true);
                     env.InitializeBinding(ie.LocalName, ns);
                 }
                 else
                 {
-                    env.CreateImportBinding(ie.LocalName, resolution.Module, resolution.BindingName);
+                    var resolution = importedModule.ResolveExport(ie.ImportName);
+                    if (resolution is null || resolution == ResolvedBinding.Ambiguous)
+                    {
+                        ExceptionHelper.ThrowSyntaxError(_realm, "Ambigous import statement for identifier " + ie.ImportName);
+                    }
+
+                    if (resolution.BindingName == "*namespace*")
+                    {
+                        var ns = GetModuleNamespace(resolution.Module);
+                        env.CreateImmutableBinding(ie.LocalName, true);
+                        env.InitializeBinding(ie.LocalName, ns);
+                    }
+                    else
+                    {
+                        env.CreateImportBinding(ie.LocalName, resolution.Module, resolution.BindingName);
+                    }
                 }
             }
         }
 
-        var moduleContext = new ExecutionContext(_environment, _environment, null, realm, null);
+        var moduleContext = new ExecutionContext(this, _environment, _environment, null, realm, null);
         _context = moduleContext;
 
         _engine.EnterExecutionContext(_context);
@@ -709,7 +747,7 @@ public sealed class JsModule : JsValue
     /// </summary>
     private Completion Execute(PromiseCapability capability = null)
     {
-        var moduleContext = new ExecutionContext(_environment, _environment, null, _realm);
+        var moduleContext = new ExecutionContext(this, _environment, _environment, null, _realm);
         if (!_hasTLA)
         {
             using (new StrictModeScope(strict: true))
@@ -901,14 +939,14 @@ public sealed class JsModule : JsValue
         return Undefined;
     }
 
-    public override bool Equals(JsValue other)
-    {
-        return false;
-    }
-
     public override object ToObject()
     {
         ExceptionHelper.ThrowNotSupportedException();
         return null;
     }
+
+    public override string ToString()
+    {
+        return $"{Type}: {Location}";
+    }
 }

+ 1 - 0
Jint/Runtime/Modules/ModuleNamespace.cs

@@ -90,6 +90,7 @@ internal sealed class ModuleNamespace : ObjectInstance
         return _exports.Contains(p);
     }
 
+    // https://tc39.es/ecma262/#sec-module-namespace-exotic-objects-get-p-receiver
     public override JsValue Get(JsValue property, JsValue receiver)
     {
         if (property.IsSymbol())

+ 16 - 0
Jint/Runtime/Modules/ModuleResolutionException.cs

@@ -0,0 +1,16 @@
+#nullable enable
+
+namespace Jint.Runtime.Modules;
+
+public sealed class ModuleResolutionException : JintException
+{
+    public string ResolverAlgorithmError { get; }
+    public string Specifier { get; }
+    
+    public ModuleResolutionException(string message, string specifier, string? parent)
+        : base($"{message} in module '{parent ?? "(null)"}': '{specifier}'")
+    {
+        ResolverAlgorithmError = message;
+        Specifier = specifier;
+    }
+}

+ 7 - 0
Jint/Runtime/Modules/ResolvedSpecifier.cs

@@ -0,0 +1,7 @@
+#nullable enable
+
+using System;
+
+namespace Jint.Runtime.Modules;
+
+public record ResolvedSpecifier(string Specifier, string Key, Uri? Uri, SpecifierType Type);

+ 8 - 0
Jint/Runtime/Modules/SpecifierType.cs

@@ -0,0 +1,8 @@
+namespace Jint.Runtime.Modules;
+
+public enum SpecifierType
+{
+    Error,
+    RelativeOrAbsolute,
+    Bare,
+}

+ 53 - 1
README.md

@@ -77,7 +77,7 @@ The entire execution engine was rebuild with performance in mind, in many cases
 - ❌ `export * as ns from`
 - ✔ `for-in` enhancements
 - ✔ `globalThis` object
--  `import`
+-  `import`
 - ❌ `import.meta`
 - ✔ Nullish coalescing operator (`??`)
 - ✔ Optional chaining
@@ -325,6 +325,58 @@ for (var i = 0; i < 10; i++)
 }
 ```
 
+## Using Modules
+
+You can use modules to `import` and `export` variables from multiple script files:
+
+```c#
+var engine = new Engine(options =>
+{
+    options.EnableModules(@"C:\Scripts");
+})
+
+var ns = engine.ImportModule("./my-module.js");
+
+var value = ns.Get("value").AsString();
+```
+
+By default, the module resolution algorithm will be restricted to the base path specified in `EnableModules`, and there is no package support. However you can provide your own packages in two ways.
+
+Defining modules using JavaScript source code:
+
+```c#
+engine.CreateModule("user", "export const name = 'John';")
+
+var ns = engine.ImportModule("user");
+
+var name = ns.Get("name").AsString();
+```
+
+Defining modules using the module builder, which allows you to export CLR classes and values from .NET:
+
+```c#
+// Create the module 'lib' with the class MyClass and the variable version
+engine.CreateModule("lib", builder => builder
+    .ExportType<MyClass>()
+    .ExportValue("version", 15)
+);
+
+// Create a user-defined module and do something with 'lib'
+engine.CreateModule("custom", @"
+    import { MyClass, version } from 'lib';
+    const x = new MyClass();
+    export const result as x.doSomething();
+");
+
+// Import the user-defined module; this will execute the import chain
+var ns = engine.ImportModule("custom");
+
+// The result contains "live" bindings to the module
+var id = ns.Get("result").AsInteger();
+```
+
+Note that you don't need to `EnableModules` if you only use modules created using `AddModule`.
+
 ## .NET Interoperability
 
 - Manipulate CLR objects from JavaScript, including: