diff --git a/Jint.Tests.Test262/Language/ModuleTests.cs b/Jint.Tests.Test262/Language/ModuleTests.cs index d600fc482c..b28be07f76 100644 --- a/Jint.Tests.Test262/Language/ModuleTests.cs +++ b/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 @@ private static void RunModuleTest(SourceFile sourceFile) 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); diff --git a/Jint.Tests.Test262/Test262Test.cs b/Jint.Tests.Test262/Test262Test.cs index cd20a8537d..018f054f34 100644 --- a/Jint.Tests.Test262/Test262Test.cs +++ b/Jint.Tests.Test262/Test262Test.cs @@ -22,7 +22,7 @@ public abstract class Test262Test { private static readonly Dictionary Sources; - private static readonly string BasePath; + protected static readonly string BasePath; private static readonly TimeZoneInfo _pacificTimeZone; diff --git a/Jint.Tests/Jint.Tests.csproj b/Jint.Tests/Jint.Tests.csproj index 13d7ef1cf1..6eca46afb0 100644 --- a/Jint.Tests/Jint.Tests.csproj +++ b/Jint.Tests/Jint.Tests.csproj @@ -5,11 +5,7 @@ ..\Jint\Jint.snk true false - - 8 + latest 612 diff --git a/Jint.Tests/Runtime/ModuleTests.cs b/Jint.Tests/Runtime/ModuleTests.cs new file mode 100644 index 0000000000..3a9fe13b68 --- /dev/null +++ b/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(() => _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(() => _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()); + _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 +} diff --git a/Jint.Tests/Runtime/Modules/DefaultModuleResolverTests.cs b/Jint.Tests/Runtime/Modules/DefaultModuleResolverTests.cs new file mode 100644 index 0000000000..c0eb2b6faf --- /dev/null +++ b/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(() => 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); + } +} diff --git a/Jint.Tests/Runtime/Scripts/modules/format-name.js b/Jint.Tests/Runtime/Scripts/modules/format-name.js new file mode 100644 index 0000000000..49db51445c --- /dev/null +++ b/Jint.Tests/Runtime/Scripts/modules/format-name.js @@ -0,0 +1,3 @@ +export function formatName(firstName, lastName) { + return `${firstName} ${lastName}`; +} diff --git a/Jint.Tests/Runtime/Scripts/modules/user.js b/Jint.Tests/Runtime/Scripts/modules/user.js new file mode 100644 index 0000000000..1393d17e63 --- /dev/null +++ b/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 }; diff --git a/Jint/Engine.Modules.cs b/Jint/Engine.Modules.cs index c9df9d4716..bd646684bd 100644 --- a/Jint/Engine.Modules.cs +++ b/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 @@ public partial class Engine { internal IModuleLoader ModuleLoader { get; set; } - private readonly Dictionary _modules = new(); + private readonly Dictionary _modules = new(); + private readonly Dictionary _builders = new(); - public JsModule LoadModule(string specifier) => LoadModule(null, specifier); + /// + /// https://tc39.es/ecma262/#sec-getactivescriptormodule + /// + 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 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; + } } -} \ No newline at end of file +} diff --git a/Jint/Engine.cs b/Jint/Engine.cs index ad5935bcfa..80dce79603 100644 --- a/Jint/Engine.cs +++ b/Jint/Engine.cs @@ -157,6 +157,7 @@ internal ExecutionContext EnterExecutionContext( PrivateEnvironmentRecord privateEnvironment) { var context = new ExecutionContext( + null, lexicalEnvironment, variableEnvironment, privateEnvironment, diff --git a/Jint/HoistingScope.cs b/Jint/HoistingScope.cs index 98b4db435c..f5197120b1 100644 --- a/Jint/HoistingScope.cs +++ b/Jint/HoistingScope.cs @@ -246,7 +246,7 @@ public void Visit(Node node, Node parent) } } - if (parent is null && variableDeclaration.Kind != VariableDeclarationKind.Var) + if ((parent is null or Module) && variableDeclaration.Kind != VariableDeclarationKind.Var) { _lexicalDeclarations ??= new List(); _lexicalDeclarations.Add(variableDeclaration); diff --git a/Jint/ModuleBuilder.cs b/Jint/ModuleBuilder.cs new file mode 100644 index 0000000000..00f6ac3d52 --- /dev/null +++ b/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 _sourceRaw = new(); + private readonly Dictionary _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() + { + ExportType(typeof(T).Name); + return this; + } + + public ModuleBuilder ExportType(string name) + { + _exports.Add(name, TypeReference.CreateTypeReference(_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 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())); + } + } + + internal void BindExportedValues(JsModule module) + { + foreach (var export in _exports) + { + module.BindExportedValue(export.Key, export.Value); + } + } +} diff --git a/Jint/Native/Function/FunctionInstance.cs b/Jint/Native/Function/FunctionInstance.cs index ab5ddc7d38..378601aa56 100644 --- a/Jint/Native/Function/FunctionInstance.cs +++ b/Jint/Native/Function/FunctionInstance.cs @@ -348,6 +348,7 @@ internal ExecutionContext PrepareForOrdinaryCall(JsValue newTarget) var calleeRealm = _realm; var calleeContext = new ExecutionContext( + null, localEnv, localEnv, _privateEnvironment, diff --git a/Jint/Native/Function/ScriptFunctionInstance.cs b/Jint/Native/Function/ScriptFunctionInstance.cs index e0a0474308..bee3488f52 100644 --- a/Jint/Native/Function/ScriptFunctionInstance.cs +++ b/Jint/Native/Function/ScriptFunctionInstance.cs @@ -138,8 +138,7 @@ ObjectInstance IConstructor.Construct(JsValue[] arguments, JsValue newTarget) { 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) diff --git a/Jint/Native/ICallable.cs b/Jint/Native/ICallable.cs index 6f36744ca2..ad17967da8 100644 --- a/Jint/Native/ICallable.cs +++ b/Jint/Native/ICallable.cs @@ -2,6 +2,6 @@ { internal interface ICallable { - JsValue Call(JsValue thisObject, JsValue[] arguments); + JsValue Call(JsValue thisObject, params JsValue[] arguments); } } diff --git a/Jint/Native/Object/ObjectInstance.cs b/Jint/Native/Object/ObjectInstance.cs index 5415291d64..c15fcf4aaf 100644 --- a/Jint/Native/Object/ObjectInstance.cs +++ b/Jint/Native/Object/ObjectInstance.cs @@ -82,6 +82,8 @@ internal ObjectClass Class get => _class; } + public JsValue this[JsValue property] => Get(property); + /// /// https://tc39.es/ecma262/#sec-construct /// @@ -215,8 +217,6 @@ public virtual IEnumerable> GetOwnProp } } - - public virtual List GetOwnPropertyKeys(Types types = Types.String | Types.Symbol) { EnsureInitialized(); diff --git a/Jint/Native/Promise/PromiseOperations.cs b/Jint/Native/Promise/PromiseOperations.cs index 98625a989d..069a84270e 100644 --- a/Jint/Native/Promise/PromiseOperations.cs +++ b/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 @@ internal static class PromiseOperations // 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 @@ internal static Action NewPromiseReactionJob(PromiseReaction reaction, JsValue v 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(); } diff --git a/Jint/Options.Extensions.cs b/Jint/Options.Extensions.cs index 2a312e5095..7ace8243c0 100644 --- a/Jint/Options.Extensions.cs +++ b/Jint/Options.Extensions.cs @@ -238,36 +238,28 @@ public static Options Configure(this Options options, Action configurati /// /// Passed Engine instance is still in construction and should not be used during call stage. /// - public static void UseHostFactory(this Options options, Func factory) where T : Host + public static Options UseHostFactory(this Options options, Func factory) where T : Host { options.Host.Factory = factory; + return options; } /// /// 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. /// - public static Options EnableModules(this Options options, bool enable = true) - { - options.Modules.Enabled = enable; - return options; - } - - /// - /// Allows to configure module loader implementation. - /// - public static Options WithModuleLoader(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)); } /// - /// Allows to configure module loader implementation. + /// Enables module loading using a custom loader implementation. /// - public static Options WithModuleLoader(this Options options, IModuleLoader moduleLoader) + public static Options EnableModules(this Options options, IModuleLoader moduleLoader) { options.Modules.ModuleLoader = moduleLoader; return options; } } -} \ No newline at end of file +} diff --git a/Jint/Options.cs b/Jint/Options.cs index b9dd57fa9f..a34ec76950 100644 --- a/Jint/Options.cs +++ b/Jint/Options.cs @@ -103,31 +103,22 @@ internal void Apply(Engine engine) 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 @@ public class HostOptions } /// - /// Module related customization, work in progress + /// Module related customization /// public class ModuleOptions { - /// - /// Indicates if modules are enabled in the current engine context, defaults to false. - /// - public bool Enabled { get; set; } - /// /// Whether to register require function to engine which will delegate to module loader, defaults to false. /// @@ -374,6 +360,6 @@ public class ModuleOptions /// /// Module loader implementation, by default exception will be thrown if module loading is not enabled. /// - public IModuleLoader? ModuleLoader { get; set; } = FailFastModuleLoader.Instance; + public IModuleLoader ModuleLoader { get; set; } = FailFastModuleLoader.Instance; } -} \ No newline at end of file +} diff --git a/Jint/Runtime/Environments/ExecutionContext.cs b/Jint/Runtime/Environments/ExecutionContext.cs index e1e96bfed9..e4b8c2b5e3 100644 --- a/Jint/Runtime/Environments/ExecutionContext.cs +++ b/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 @@ internal ExecutionContext( 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 @@ internal ExecutionContext( 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); } /// diff --git a/Jint/Runtime/Environments/FunctionEnvironmentRecord.cs b/Jint/Runtime/Environments/FunctionEnvironmentRecord.cs index 14773f555b..81016c4548 100644 --- a/Jint/Runtime/Environments/FunctionEnvironmentRecord.cs +++ b/Jint/Runtime/Environments/FunctionEnvironmentRecord.cs @@ -345,7 +345,7 @@ private void HandleAssignmentPatternOrExpression( 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; diff --git a/Jint/Runtime/Environments/ModuleEnvironmentRecord.cs b/Jint/Runtime/Environments/ModuleEnvironmentRecord.cs index fff1586631..df2bf7019f 100644 --- a/Jint/Runtime/Environments/ModuleEnvironmentRecord.cs +++ b/Jint/Runtime/Environments/ModuleEnvironmentRecord.cs @@ -27,26 +27,27 @@ public void CreateImportBinding(string importName, JsModule module, string name) _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; diff --git a/Jint/Runtime/ExceptionHelper.cs b/Jint/Runtime/ExceptionHelper.cs index cb97da118c..150ba62adc 100644 --- a/Jint/Runtime/ExceptionHelper.cs +++ b/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 @@ public static void ThrowSyntaxError(Realm realm, string message = null) 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 @@ public static void ThrowExecutionCanceledException() { throw new ExecutionCanceledException(); } + + [DoesNotReturn] + public static void ThrowModuleResolutionException(string resolverAlgorithmError, string specifier, string parent) + { + throw new ModuleResolutionException(resolverAlgorithmError, specifier, parent); + } } -} \ No newline at end of file +} diff --git a/Jint/Runtime/ExecutionContextStack.cs b/Jint/Runtime/ExecutionContextStack.cs index f87a171b00..f05f765a33 100644 --- a/Jint/Runtime/ExecutionContextStack.cs +++ b/Jint/Runtime/ExecutionContextStack.cs @@ -37,5 +37,21 @@ public void ReplaceTopVariableEnvironment(EnvironmentRecord newEnv) [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; + } } -} \ No newline at end of file +} diff --git a/Jint/Runtime/Host.cs b/Jint/Runtime/Host.cs index 75745ad51e..05a9ba4272 100644 --- a/Jint/Runtime/Host.cs +++ b/Jint/Runtime/Host.cs @@ -35,6 +35,7 @@ protected virtual void InitializeHostDefinedRealm() var realm = CreateRealm(); var newContext = new ExecutionContext( + scriptOrModule: null, lexicalEnvironment: realm.GlobalEnv, variableEnvironment: realm.GlobalEnv, privateEnvironment: null, @@ -106,7 +107,7 @@ public virtual void EnsureCanCompileStrings(Realm callerRealm, Realm evalRealm) /// protected internal virtual JsModule ResolveImportedModule(JsModule referencingModule, string specifier) { - return Engine.LoadModule(referencingModule._location, specifier); + return Engine.LoadModule(referencingModule.Location, specifier); } /// @@ -121,7 +122,7 @@ internal virtual void ImportModuleDynamically(JsModule referencingModule, string try { - Engine.LoadModule(referencingModule._location, specifier); + Engine.LoadModule(referencingModule.Location, specifier); promise.Resolve(JsValue.Undefined); } @@ -164,7 +165,7 @@ internal virtual void FinishDynamicImport(JsModule referencingModule, string spe return JsValue.Undefined; }, 0, PropertyFlag.Configurable); - PromiseOperations.PerformPromiseThen(Engine, innerPromise, onFulfilled, onRejected, null); + PromiseOperations.PerformPromiseThen(Engine, innerPromise, onFulfilled, onRejected, promiseCapability); } } } diff --git a/Jint/Runtime/IScriptOrModule.Extensions.cs b/Jint/Runtime/IScriptOrModule.Extensions.cs new file mode 100644 index 0000000000..2e65065bcd --- /dev/null +++ b/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; + } +} diff --git a/Jint/Runtime/IScriptOrModule.cs b/Jint/Runtime/IScriptOrModule.cs new file mode 100644 index 0000000000..278a61c983 --- /dev/null +++ b/Jint/Runtime/IScriptOrModule.cs @@ -0,0 +1,5 @@ +namespace Jint.Runtime; + +internal interface IScriptOrModule +{ +} diff --git a/Jint/Runtime/Interpreter/JintStatementList.cs b/Jint/Runtime/Interpreter/JintStatementList.cs index 57d8f3184f..5a40dc4d79 100644 --- a/Jint/Runtime/Interpreter/JintStatementList.cs +++ b/Jint/Runtime/Interpreter/JintStatementList.cs @@ -42,11 +42,13 @@ private void Initialize(EvaluationContext context) 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; diff --git a/Jint/Runtime/Interpreter/Statements/JintExportAllDeclaration.cs b/Jint/Runtime/Interpreter/Statements/JintExportAllDeclaration.cs new file mode 100644 index 0000000000..6bbbdc7dd4 --- /dev/null +++ b/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 +{ + public JintExportAllDeclaration(ExportAllDeclaration statement) : base(statement) + { + } + + protected override void Initialize(EvaluationContext context) + { + } + + protected override Completion ExecuteInternal(EvaluationContext context) + { + return Completion.Empty(); + } +} diff --git a/Jint/Runtime/Interpreter/Statements/JintExportDefaultDeclaration.cs b/Jint/Runtime/Interpreter/Statements/JintExportDefaultDeclaration.cs new file mode 100644 index 0000000000..7f3de56a69 --- /dev/null +++ b/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 +{ + 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(); + } +} diff --git a/Jint/Runtime/Interpreter/Statements/JintExportNamedDeclaration.cs b/Jint/Runtime/Interpreter/Statements/JintExportNamedDeclaration.cs new file mode 100644 index 0000000000..89cc09f445 --- /dev/null +++ b/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 +{ + 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) + ); + } + } + } + + /// + /// 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); + + 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); + } +} diff --git a/Jint/Runtime/Interpreter/Statements/JintImportDeclaration.cs b/Jint/Runtime/Interpreter/Statements/JintImportDeclaration.cs new file mode 100644 index 0000000000..2d55f66a75 --- /dev/null +++ b/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 +{ + 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); + } +} diff --git a/Jint/Runtime/Interpreter/Statements/JintStatement.cs b/Jint/Runtime/Interpreter/Statements/JintStatement.cs index 0d8b0832cb..8eba98c63f 100644 --- a/Jint/Runtime/Interpreter/Statements/JintStatement.cs +++ b/Jint/Runtime/Interpreter/Statements/JintStatement.cs @@ -90,10 +90,10 @@ protected internal static JintStatement Build(Statement statement) 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 }; diff --git a/Jint/Runtime/JavaScriptException.cs b/Jint/Runtime/JavaScriptException.cs index d3fee8f2de..156959caeb 100644 --- a/Jint/Runtime/JavaScriptException.cs +++ b/Jint/Runtime/JavaScriptException.cs @@ -35,6 +35,13 @@ public JavaScriptException(JsValue error) Error = error; } + internal JavaScriptException SetLocation(Location location) + { + Location = location; + + return this; + } + internal JavaScriptException SetCallstack(Engine engine, Location location) { Location = location; diff --git a/Jint/Runtime/Modules/DefaultModuleLoader.cs b/Jint/Runtime/Modules/DefaultModuleLoader.cs index 0a55f4f993..558a313a82 100644 --- a/Jint/Runtime/Modules/DefaultModuleLoader.cs +++ b/Jint/Runtime/Modules/DefaultModuleLoader.cs @@ -7,42 +7,132 @@ 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 @@ protected virtual ModuleLoaderResult LoadModule(Engine engine, Uri location) } 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("/"); } } diff --git a/Jint/Runtime/Modules/FailFastModuleLoader.cs b/Jint/Runtime/Modules/FailFastModuleLoader.cs index ed850dae2a..3bf2b3e114 100644 --- a/Jint/Runtime/Modules/FailFastModuleLoader.cs +++ b/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() diff --git a/Jint/Runtime/Modules/IModuleLoader.cs b/Jint/Runtime/Modules/IModuleLoader.cs index 41037ea3e6..375e3b8f83 100644 --- a/Jint/Runtime/Modules/IModuleLoader.cs +++ b/Jint/Runtime/Modules/IModuleLoader.cs @@ -1,22 +1,21 @@ #nullable enable -using System; using Esprima.Ast; namespace Jint.Runtime.Modules; -/// -/// Module loading result. -/// -public readonly record struct ModuleLoaderResult(Module Module, Uri Location); - /// /// Module loader interface that allows defining how module loadings requests are handled. /// public interface IModuleLoader { + /// + /// Resolves a specifier to a path or module + /// + ResolvedSpecifier Resolve(string? referencingModuleLocation, string specifier); + /// /// Loads a module from given location. /// - public ModuleLoaderResult LoadModule(Engine engine, string location, string? referencingLocation); -} \ No newline at end of file + public Module LoadModule(Engine engine, ResolvedSpecifier resolved); +} diff --git a/Jint/Runtime/Modules/JsModule.cs b/Jint/Runtime/Modules/JsModule.cs index 9fd5a718aa..2fde9ee713 100644 --- a/Jint/Runtime/Modules/JsModule.cs +++ b/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 @@ string ExportName /// https://tc39.es/ecma262/#sec-cyclic-module-records /// https://tc39.es/ecma262/#sec-source-text-module-records /// -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 _localExportEntries; private readonly List _indirectExportEntries; private readonly List _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 @@ internal JsModule(Engine engine, Realm realm, Module source, string location, bo _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 @@ internal JsModule(Engine engine, Realm realm, Module source, string location, bo } + public string Location { get; } internal ModuleStatus Status { get; private set; } /// @@ -131,6 +132,12 @@ private static ObjectInstance CreateModuleNamespace(JsModule module, List /// https://tc39.es/ecma262/#sec-getexportednames /// @@ -148,13 +155,13 @@ public List GetExportedNames(List exportStarSet = null) 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 List GetExportedNames(List exportStarSet = null) 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 @@ internal ResolvedBinding ResolveExport(string exportName, List private int Link(JsModule module, Stack 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 @@ ModuleStatus.EvaluatingAsync or { 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 @@ private Completion Evaluate(JsModule module, Stack stack, int index, r 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 @@ private Completion Evaluate(JsModule module, Stack stack, int index, r } } + Completion completion; + if(module._pendingAsyncDependencies > 0 || module._hasTLA) { if (module._asyncEvaluation) @@ -535,16 +571,16 @@ private Completion Evaluate(JsModule module, Stack stack, int index, r 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 @@ private Completion Evaluate(JsModule module, Stack stack, int index, r } } - return new Completion(CompletionType.Normal, index, null, default); - + return completion; } /// @@ -600,38 +635,41 @@ private void InitializeEnvironment() 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 @@ private void InitializeEnvironment() /// 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 @@ private JsValue AsyncModuleExecutionRejected(JsValue thisObj, JsValue[] argument 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}"; + } } diff --git a/Jint/Runtime/Modules/ModuleNamespace.cs b/Jint/Runtime/Modules/ModuleNamespace.cs index 3237eb7839..a836d7d1b4 100644 --- a/Jint/Runtime/Modules/ModuleNamespace.cs +++ b/Jint/Runtime/Modules/ModuleNamespace.cs @@ -90,6 +90,7 @@ public override bool HasProperty(JsValue property) 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()) diff --git a/Jint/Runtime/Modules/ModuleResolutionException.cs b/Jint/Runtime/Modules/ModuleResolutionException.cs new file mode 100644 index 0000000000..5ec212ac04 --- /dev/null +++ b/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; + } +} \ No newline at end of file diff --git a/Jint/Runtime/Modules/ResolvedSpecifier.cs b/Jint/Runtime/Modules/ResolvedSpecifier.cs new file mode 100644 index 0000000000..30c6783c53 --- /dev/null +++ b/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); diff --git a/Jint/Runtime/Modules/SpecifierType.cs b/Jint/Runtime/Modules/SpecifierType.cs new file mode 100644 index 0000000000..3f428cd1b9 --- /dev/null +++ b/Jint/Runtime/Modules/SpecifierType.cs @@ -0,0 +1,8 @@ +namespace Jint.Runtime.Modules; + +public enum SpecifierType +{ + Error, + RelativeOrAbsolute, + Bare, +} diff --git a/README.md b/README.md index 1f05e6c35c..b9979f3549 100644 --- a/README.md +++ b/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() + .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: