Browse Source

Implement JSON modules (#1711)

Marko Lahma 1 year ago
parent
commit
7678879055

+ 0 - 1
Jint.Tests.Test262/Test262Harness.settings.json

@@ -11,7 +11,6 @@
     "generators",
     "import-assertions",
     "iterator-helpers",
-    "json-modules",
     "regexp-duplicate-named-groups",
     "regexp-lookbehind",
     "regexp-unicode-property-escapes",

+ 10 - 34
Jint.Tests.Test262/Test262ModuleLoader.cs

@@ -1,14 +1,12 @@
 #nullable enable
 
-using Esprima;
-using Esprima.Ast;
 using Jint.Runtime;
 using Jint.Runtime.Modules;
 using Zio;
 
 namespace Jint.Tests.Test262;
 
-internal sealed class Test262ModuleLoader : IModuleLoader
+internal sealed class Test262ModuleLoader : ModuleLoader
 {
     private readonly IFileSystem _fileSystem;
     private readonly string _basePath;
@@ -19,44 +17,22 @@ internal sealed class Test262ModuleLoader : IModuleLoader
         _basePath = "/test/" + basePath.TrimStart('\\').TrimStart('/');
     }
 
-    public ResolvedSpecifier Resolve(string? referencingModuleLocation, string specifier)
+    public override ResolvedSpecifier Resolve(string? referencingModuleLocation, ModuleRequest moduleRequest)
     {
-        return new ResolvedSpecifier(referencingModuleLocation ?? "", specifier ?? "", null, SpecifierType.Bare);
+        return new ResolvedSpecifier(moduleRequest, moduleRequest.Specifier, null, SpecifierType.Bare);
     }
 
-    public Module LoadModule(Engine engine, ResolvedSpecifier resolved)
+    protected override string LoadModuleContents(Engine engine, ResolvedSpecifier resolved)
     {
-        Module module;
-        try
+        lock (_fileSystem)
         {
-            string code;
-            lock (_fileSystem)
+            var fileName = Path.Combine(_basePath, resolved.Key).Replace('\\', '/');
+            if (!_fileSystem.FileExists(fileName))
             {
-                var fileName = Path.Combine(_basePath, resolved.Key).Replace('\\', '/');
-                using var stream = new StreamReader(_fileSystem.OpenFile(fileName, FileMode.Open, FileAccess.Read));
-                code = stream.ReadToEnd();
+                ExceptionHelper.ThrowModuleResolutionException("Module Not Found", resolved.ModuleRequest.Specifier, parent: null, fileName);
             }
-
-            var parserOptions = new ParserOptions
-            {
-                RegExpParseMode = RegExpParseMode.AdaptToInterpreted,
-                Tolerant = true
-            };
-
-            module = new JavaScriptParser(parserOptions).ParseModule(code, source: resolved.Uri?.LocalPath!);
-        }
-        catch (ParserException ex)
-        {
-            ExceptionHelper.ThrowSyntaxError(engine.Realm, $"Error while loading module: error in module '{resolved.Uri?.LocalPath}': {ex.Error}");
-            module = null;
-        }
-        catch (Exception ex)
-        {
-            var message = $"Could not load module {resolved.Uri?.LocalPath}: {ex.Message}";
-            ExceptionHelper.ThrowJavaScriptException(engine, message, (Location) default);
-            module = null;
+            using var stream = new StreamReader(_fileSystem.OpenFile(fileName, FileMode.Open, FileAccess.Read));
+            return stream.ReadToEnd();
         }
-
-        return module;
     }
 }

+ 9 - 9
Jint.Tests/Runtime/Modules/DefaultModuleLoaderTests.cs

@@ -5,17 +5,17 @@ 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")]
+    [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);
+        var resolved = resolver.Resolve("file:///project/folder/script.js", new ModuleRequest(specifier, []));
 
-        Assert.Equal(specifier, resolved.Specifier);
+        Assert.Equal(specifier, resolved.ModuleRequest.Specifier);
         Assert.Equal(expectedUri, resolved.Key);
         Assert.Equal(expectedUri, resolved.Uri?.AbsoluteUri);
         Assert.Equal(SpecifierType.RelativeOrAbsolute, resolved.Type);
@@ -30,7 +30,7 @@ public class DefaultModuleLoaderTests
     {
         var resolver = new DefaultModuleLoader("file:///project");
 
-        var exc = Assert.Throws<ModuleResolutionException>(() => resolver.Resolve("file:///project/folder/script.js", specifier));
+        var exc = Assert.Throws<ModuleResolutionException>(() => resolver.Resolve("file:///project/folder/script.js", new ModuleRequest(specifier, [])));
         Assert.StartsWith(exc.ResolverAlgorithmError, "Unauthorized Module Path");
         Assert.StartsWith(exc.Specifier, specifier);
     }
@@ -40,9 +40,9 @@ public class DefaultModuleLoaderTests
     {
         var resolver = new DefaultModuleLoader("/");
 
-        var resolved = resolver.Resolve(null, "my-module");
+        var resolved = resolver.Resolve(null, new ModuleRequest("my-module", []));
 
-        Assert.Equal("my-module", resolved.Specifier);
+        Assert.Equal("my-module", resolved.ModuleRequest.Specifier);
         Assert.Equal("my-module", resolved.Key);
         Assert.Equal(null, resolved.Uri?.AbsoluteUri);
         Assert.Equal(SpecifierType.Bare, resolved.Type);

+ 17 - 12
Jint/Engine.Modules.cs

@@ -23,9 +23,10 @@ namespace Jint
             return _executionContexts?.GetActiveScriptOrModule();
         }
 
-        internal ModuleRecord LoadModule(string? referencingModuleLocation, string specifier)
+        internal ModuleRecord LoadModule(string? referencingModuleLocation, ModuleRequest request)
         {
-            var moduleResolution = ModuleLoader.Resolve(referencingModuleLocation, specifier);
+            var specifier = request.Specifier;
+            var moduleResolution = ModuleLoader.Resolve(referencingModuleLocation, request);
 
             if (_modules.TryGetValue(moduleResolution.Key, out var module))
             {
@@ -59,10 +60,9 @@ namespace Jint
             return module;
         }
 
-        private SourceTextModuleRecord LoadFromModuleLoader(ResolvedSpecifier moduleResolution)
+        private ModuleRecord LoadFromModuleLoader(ResolvedSpecifier moduleResolution)
         {
-            var parsedModule = ModuleLoader.LoadModule(this, moduleResolution);
-            var module = new SourceTextModuleRecord(this, Realm, parsedModule, moduleResolution.Uri?.LocalPath, false);
+            var module = ModuleLoader.LoadModule(this, moduleResolution);
             _modules[moduleResolution.Key] = module;
             return module;
         }
@@ -88,30 +88,35 @@ namespace Jint
 
         public ObjectInstance ImportModule(string specifier)
         {
-            return ImportModule(specifier, null);
+            return ImportModule(specifier, referencingModuleLocation: null);
         }
 
         internal ObjectInstance ImportModule(string specifier, string? referencingModuleLocation)
         {
-            var moduleResolution = ModuleLoader.Resolve(referencingModuleLocation, specifier);
+            return ImportModule(new ModuleRequest(specifier, []), referencingModuleLocation);
+        }
+
+        internal ObjectInstance ImportModule(ModuleRequest request, string? referencingModuleLocation)
+        {
+            var moduleResolution = ModuleLoader.Resolve(referencingModuleLocation, request);
 
             if (!_modules.TryGetValue(moduleResolution.Key, out var module))
             {
-                module = LoadModule(null, specifier);
+                module = LoadModule(referencingModuleLocation: null, request);
             }
 
             if (module is not CyclicModuleRecord cyclicModule)
             {
-                LinkModule(specifier, module);
-                EvaluateModule(specifier, module);
+                LinkModule(request.Specifier, module);
+                EvaluateModule(request.Specifier, module);
             }
             else if (cyclicModule.Status == ModuleStatus.Unlinked)
             {
-                LinkModule(specifier, cyclicModule);
+                LinkModule(request.Specifier, cyclicModule);
 
                 if (cyclicModule.Status == ModuleStatus.Linked)
                 {
-                    ExecuteWithConstraints(true, () => EvaluateModule(specifier, cyclicModule));
+                    ExecuteWithConstraints(true, () => EvaluateModule(request.Specifier, cyclicModule));
                 }
 
                 if (cyclicModule.Status != ModuleStatus.Evaluated)

+ 31 - 10
Jint/EsprimaExtensions.cs

@@ -330,25 +330,42 @@ namespace Jint
         {
             var source = import.Source.StringValue!;
             var specifiers = import.Specifiers;
-            requestedModules.Add(new ModuleRequest(source, []));
+            var attributes = GetAttributes(import.Attributes);
+            requestedModules.Add(new ModuleRequest(source, attributes));
 
             foreach (var specifier in specifiers)
             {
                 switch (specifier)
                 {
                     case ImportNamespaceSpecifier namespaceSpecifier:
-                        importEntries.Add(new ImportEntry(source, "*", namespaceSpecifier.Local.GetModuleKey()));
+                        importEntries.Add(new ImportEntry(new ModuleRequest(source, attributes), "*", namespaceSpecifier.Local.GetModuleKey()));
                         break;
                     case ImportSpecifier importSpecifier:
-                        importEntries.Add(new ImportEntry(source, importSpecifier.Imported.GetModuleKey(), importSpecifier.Local.GetModuleKey()));
+                        importEntries.Add(new ImportEntry(new ModuleRequest(source, attributes), importSpecifier.Imported.GetModuleKey(), importSpecifier.Local.GetModuleKey()!));
                         break;
                     case ImportDefaultSpecifier defaultSpecifier:
-                        importEntries.Add(new ImportEntry(source, "default", defaultSpecifier.Local.GetModuleKey()));
+                        importEntries.Add(new ImportEntry(new ModuleRequest(source, attributes), "default", defaultSpecifier.Local.GetModuleKey()));
                         break;
                 }
             }
         }
 
+        private static ModuleImportAttribute[] GetAttributes(in NodeList<ImportAttribute> importAttributes)
+        {
+            if (importAttributes.Count == 0)
+            {
+                return Array.Empty<ModuleImportAttribute>();
+            }
+
+            var attributes = new ModuleImportAttribute[importAttributes.Count];
+            for (var i = 0; i < importAttributes.Count; i++)
+            {
+                var attribute = importAttributes[i];
+                attributes[i] = new ModuleImportAttribute(attribute.Key.ToString(), attribute.Value.StringValue!);
+            }
+            return attributes;
+        }
+
         internal static void GetExportEntries(this ExportDeclaration export, List<ExportEntry> exportEntries, HashSet<ModuleRequest> requestedModules)
         {
             switch (export)
@@ -359,13 +376,17 @@ namespace Jint
                 case ExportAllDeclaration allDeclaration:
                     //Note: there is a pending PR for Esprima to support exporting an imported modules content as a namespace i.e. 'export * as ns from "mod"'
                     requestedModules.Add(new ModuleRequest(allDeclaration.Source.StringValue!, []));
-                    exportEntries.Add(new(allDeclaration.Exported?.GetModuleKey(), allDeclaration.Source.StringValue, "*", null));
+                    exportEntries.Add(new(allDeclaration.Exported?.GetModuleKey(), new ModuleRequest(allDeclaration.Source.StringValue!, []), "*", null));
                     break;
                 case ExportNamedDeclaration namedDeclaration:
                     ref readonly var specifiers = ref namedDeclaration.Specifiers;
                     if (specifiers.Count == 0)
                     {
-                        GetExportEntries(false, namedDeclaration.Declaration!, exportEntries, namedDeclaration.Source?.StringValue);
+                        ModuleRequest? moduleRequest = namedDeclaration.Source != null
+                            ? new ModuleRequest(namedDeclaration.Source?.StringValue!, [])
+                            : null;
+
+                        GetExportEntries(false, namedDeclaration.Declaration!, exportEntries, moduleRequest);
                     }
                     else
                     {
@@ -374,7 +395,7 @@ namespace Jint
                             var specifier = specifiers[i];
                             if (namedDeclaration.Source != null)
                             {
-                                exportEntries.Add(new(specifier.Exported.GetModuleKey(), namedDeclaration.Source.StringValue, specifier.Local.GetModuleKey(), null));
+                                exportEntries.Add(new(specifier.Exported.GetModuleKey(), new ModuleRequest(namedDeclaration.Source.StringValue!, []), specifier.Local.GetModuleKey(), null));
                             }
                             else
                             {
@@ -392,7 +413,7 @@ namespace Jint
             }
         }
 
-        private static void GetExportEntries(bool defaultExport, StatementListItem declaration, List<ExportEntry> exportEntries, string? moduleRequest = null)
+        private static void GetExportEntries(bool defaultExport, StatementListItem declaration, List<ExportEntry> exportEntries, ModuleRequest? moduleRequest = null)
         {
             var names = GetExportNames(declaration);
 
@@ -444,9 +465,9 @@ namespace Jint
             return result;
         }
 
-        private static string? GetModuleKey(this Expression expression)
+        private static string GetModuleKey(this Expression expression)
         {
-            return (expression as Identifier)?.Name ?? (expression as Literal)?.StringValue;
+            return (expression as Identifier)?.Name ?? (expression as Literal)!.StringValue!;
         }
 
         internal readonly record struct Record(JsValue Key, ScriptFunctionInstance Closure);

+ 1 - 1
Jint/HoistingScope.cs

@@ -134,7 +134,7 @@ namespace Jint
 
             importEntries = treeWalker._importEntries;
             requestedModules = treeWalker._requestedModules ?? [];
-            var importedBoundNames = new HashSet<string>(StringComparer.Ordinal);
+            var importedBoundNames = new HashSet<string?>(StringComparer.Ordinal);
 
             if (importEntries != null)
             {

+ 1 - 1
Jint/Native/ShadowRealm/ShadowRealm.cs

@@ -277,7 +277,7 @@ public sealed class ShadowRealm : ObjectInstance
         // 4. If runningContext is not already suspended, suspend runningContext.
 
         _engine.EnterExecutionContext(_executionContext);
-        _engine._host.LoadImportedModule(null, new ModuleRequest(specifierString, new List<KeyValuePair<string, JsValue>>()), innerCapability);
+        _engine._host.LoadImportedModule(null, new ModuleRequest(specifierString, []), innerCapability);
         _engine.LeaveExecutionContext();
 
         var onFulfilled = new StepsFunction(_engine, callerRealm, exportNameString);

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

@@ -51,7 +51,7 @@ internal sealed class ModuleEnvironmentRecord : DeclarativeEnvironmentRecord
     {
         if (_importBindings.TryGetValue(name.Key, out var indirectBinding))
         {
-            value = indirectBinding.Module._environment.GetBindingValue(indirectBinding.BindingName, true);
+            value = indirectBinding.Module._environment.GetBindingValue(indirectBinding.BindingName, strict: true);
             binding = new(value, canBeDeleted: false, mutable: false, strict: true);
             return true;
         }

+ 3 - 3
Jint/Runtime/Host.cs

@@ -118,9 +118,9 @@ namespace Jint.Runtime
         /// <summary>
         /// https://tc39.es/ecma262/#sec-GetImportedModule
         /// </summary>
-        internal virtual ModuleRecord GetImportedModule(IScriptOrModule? referrer, string specifier)
+        internal virtual ModuleRecord GetImportedModule(IScriptOrModule? referrer, ModuleRequest request)
         {
-            return Engine.LoadModule(referrer?.Location, specifier);
+            return Engine.LoadModule(referrer?.Location, request);
         }
 
         /// <summary>
@@ -151,7 +151,7 @@ namespace Jint.Runtime
         {
             var onFulfilled = new ClrFunctionInstance(Engine, "", (thisObj, args) =>
             {
-                var moduleRecord = GetImportedModule(referrer, moduleRequest.Specifier);
+                var moduleRecord = GetImportedModule(referrer, moduleRequest);
                 try
                 {
                     var ns = ModuleRecord.GetModuleNamespace(moduleRecord);

+ 10 - 9
Jint/Runtime/Interpreter/Expressions/JintImportExpression.cs

@@ -40,37 +40,38 @@ internal sealed class JintImportExpression : JintExpression
         {
             var specifierString = TypeConverter.ToString(specifier);
 
-            var attributes = new List<KeyValuePair<string, JsValue>>();
+            var attributes = new List<ModuleImportAttribute>();
             if (!options.IsUndefined())
             {
-                if (options is not JsObject o)
+                if (!options.IsObject())
                 {
                     ExceptionHelper.ThrowTypeError(context.Engine.Realm, "Invalid options object");
                     return JsValue.Undefined;
                 }
 
-                var attributesObj = o.Get("with");
+                var attributesObj = options.Get("with");
                 if (!attributesObj.IsUndefined())
                 {
-                    if (attributesObj is not JsObject oi)
+                    if (attributesObj is not ObjectInstance oi)
                     {
                         ExceptionHelper.ThrowTypeError(context.Engine.Realm, "Invalid options.with object");
                         return JsValue.Undefined;
                     }
 
                     var entries = oi.EnumerableOwnProperties(ObjectInstance.EnumerableOwnPropertyNamesKind.KeyValue);
+                    attributes.Capacity = (int) entries.Length;
                     foreach (var entry in entries)
                     {
                         var key = entry.Get("0");
                         var value = entry.Get("1");
 
-                        if (!key.IsString())
+                        if (!value.IsString())
                         {
-                            ExceptionHelper.ThrowTypeError(context.Engine.Realm, "Invalid option key " + key);
+                            ExceptionHelper.ThrowTypeError(context.Engine.Realm, "Invalid option value " + value);
                             return JsValue.Undefined;
                         }
 
-                        attributes.Add(new KeyValuePair<string, JsValue>(key.ToString(), value));
+                        attributes.Add(new ModuleImportAttribute(key.ToString(), TypeConverter.ToString(value)));
                     }
 
                     if (!AllImportAttributesSupported(context.Engine._host, attributes))
@@ -82,7 +83,7 @@ internal sealed class JintImportExpression : JintExpression
                 }
             }
 
-            var moduleRequest = new ModuleRequest(Specifier: specifierString, Attributes: attributes);
+            var moduleRequest = new ModuleRequest(Specifier: specifierString, Attributes: attributes.ToArray());
             context.Engine._host.LoadImportedModule(referrer, moduleRequest, promiseCapability);
         }
         catch (JavaScriptException e)
@@ -93,7 +94,7 @@ internal sealed class JintImportExpression : JintExpression
         return promiseCapability.PromiseInstance;
     }
 
-    private static bool AllImportAttributesSupported(Host host, List<KeyValuePair<string, JsValue>> attributes)
+    private static bool AllImportAttributesSupported(Host host, List<ModuleImportAttribute> attributes)
     {
         var supported = host.GetSupportedImportAttributes();
         foreach (var pair in attributes)

+ 32 - 36
Jint/Runtime/Modules/CyclicModuleRecord.cs

@@ -1,7 +1,6 @@
 #nullable disable
 
 using Esprima;
-using Esprima.Ast;
 using Jint.Native;
 using Jint.Native.Promise;
 using Jint.Runtime.Descriptors;
@@ -35,7 +34,7 @@ public abstract class CyclicModuleRecord : ModuleRecord
 
     internal JsValue _evalResult;
 
-    internal CyclicModuleRecord(Engine engine, Realm realm, Module source, string location, bool async) : base(engine, realm, location)
+    internal CyclicModuleRecord(Engine engine, Realm realm, string location, bool async) : base(engine, realm, location)
     {
     }
 
@@ -194,7 +193,7 @@ public abstract class CyclicModuleRecord : ModuleRecord
 
         foreach (var request in _requestedModules)
         {
-            var requiredModule = _engine._host.GetImportedModule(this, request.Specifier);
+            var requiredModule = _engine._host.GetImportedModule(this, request);
 
             index = requiredModule.InnerModuleLinking(stack, index);
 
@@ -285,7 +284,7 @@ public abstract class CyclicModuleRecord : ModuleRecord
 
         foreach (var required in _requestedModules)
         {
-            var requiredModule = _engine._host.GetImportedModule(this, required.Specifier);
+            var requiredModule = _engine._host.GetImportedModule(this, required);
 
             var result = requiredModule.InnerModuleEvaluation(stack, index, ref asyncEvalOrder);
             if (result.Type != CompletionType.Normal)
@@ -295,46 +294,43 @@ public abstract class CyclicModuleRecord : ModuleRecord
 
             index = TypeConverter.ToInt32(result.Value);
 
-            if (requiredModule is not CyclicModuleRecord requiredCyclicModule)
-            {
-                ExceptionHelper.ThrowNotImplementedException($"Resolving modules of type {requiredModule.GetType()} is not implemented");
-                continue;
-            }
-
-            if (requiredCyclicModule.Status != ModuleStatus.Evaluating &&
-                requiredCyclicModule.Status != ModuleStatus.EvaluatingAsync &&
-                requiredCyclicModule.Status != ModuleStatus.Evaluated)
-            {
-                ExceptionHelper.ThrowInvalidOperationException($"Error while evaluating module: Module is in an invalid state: {requiredCyclicModule.Status}");
-            }
-
-            if (requiredCyclicModule.Status == ModuleStatus.Evaluating && !stack.Contains(requiredCyclicModule))
+            if (requiredModule is CyclicModuleRecord requiredCyclicModule)
             {
-                ExceptionHelper.ThrowInvalidOperationException($"Error while evaluating module: Module is in an invalid state: {requiredCyclicModule.Status}");
-            }
+                if (requiredCyclicModule.Status != ModuleStatus.Evaluating &&
+                    requiredCyclicModule.Status != ModuleStatus.EvaluatingAsync &&
+                    requiredCyclicModule.Status != ModuleStatus.Evaluated)
+                {
+                    ExceptionHelper.ThrowInvalidOperationException($"Error while evaluating module: Module is in an invalid state: {requiredCyclicModule.Status}");
+                }
 
-            if (requiredCyclicModule.Status == ModuleStatus.Evaluating)
-            {
-                _dfsAncestorIndex = Math.Min(_dfsAncestorIndex, requiredCyclicModule._dfsAncestorIndex);
-            }
-            else
-            {
-                requiredCyclicModule = requiredCyclicModule._cycleRoot;
-                if (requiredCyclicModule.Status is not (ModuleStatus.EvaluatingAsync or ModuleStatus.Evaluated))
+                if (requiredCyclicModule.Status == ModuleStatus.Evaluating && !stack.Contains(requiredCyclicModule))
                 {
-                    ExceptionHelper.ThrowInvalidOperationException("Error while evaluating module: Module is in an invalid state");
+                    ExceptionHelper.ThrowInvalidOperationException($"Error while evaluating module: Module is in an invalid state: {requiredCyclicModule.Status}");
                 }
 
-                if (requiredCyclicModule._evalError != null)
+                if (requiredCyclicModule.Status == ModuleStatus.Evaluating)
                 {
-                    return requiredCyclicModule._evalError.Value;
+                    _dfsAncestorIndex = Math.Min(_dfsAncestorIndex, requiredCyclicModule._dfsAncestorIndex);
                 }
-            }
+                else
+                {
+                    requiredCyclicModule = requiredCyclicModule._cycleRoot;
+                    if (requiredCyclicModule.Status is not (ModuleStatus.EvaluatingAsync or ModuleStatus.Evaluated))
+                    {
+                        ExceptionHelper.ThrowInvalidOperationException("Error while evaluating module: Module is in an invalid state");
+                    }
 
-            if (requiredCyclicModule._asyncEvaluation)
-            {
-                _pendingAsyncDependencies++;
-                requiredCyclicModule._asyncParentModules.Add(this);
+                    if (requiredCyclicModule._evalError != null)
+                    {
+                        return requiredCyclicModule._evalError.Value;
+                    }
+                }
+
+                if (requiredCyclicModule._asyncEvaluation)
+                {
+                    _pendingAsyncDependencies++;
+                    requiredCyclicModule._asyncParentModules.Add(this);
+                }
             }
         }
 

+ 12 - 34
Jint/Runtime/Modules/DefaultModuleLoader.cs

@@ -1,9 +1,6 @@
-using Esprima;
-using Esprima.Ast;
-
 namespace Jint.Runtime.Modules;
 
-public sealed class DefaultModuleLoader : IModuleLoader
+public class DefaultModuleLoader : ModuleLoader
 {
     private readonly Uri _basePath;
     private readonly bool _restrictToBasePath;
@@ -32,7 +29,7 @@ public sealed class DefaultModuleLoader : IModuleLoader
             _basePath = temp;
         }
 
-        if (_basePath.AbsolutePath[_basePath.AbsolutePath.Length - 1] != '/')
+        if (_basePath.AbsolutePath[^1] != '/')
         {
             var uriBuilder = new UriBuilder(_basePath);
             uriBuilder.Path += '/';
@@ -40,8 +37,9 @@ public sealed class DefaultModuleLoader : IModuleLoader
         }
     }
 
-    public ResolvedSpecifier Resolve(string? referencingModuleLocation, string specifier)
+    public override ResolvedSpecifier Resolve(string? referencingModuleLocation, ModuleRequest moduleRequest)
     {
+        var specifier = moduleRequest.Specifier;
         if (string.IsNullOrEmpty(specifier))
         {
             ExceptionHelper.ThrowModuleResolutionException("Invalid Module Specifier", specifier, referencingModuleLocation);
@@ -67,7 +65,7 @@ public sealed class DefaultModuleLoader : IModuleLoader
         else
         {
             return new ResolvedSpecifier(
-                specifier,
+                moduleRequest,
                 specifier,
                 Uri: null,
                 SpecifierType.Bare
@@ -96,53 +94,33 @@ public sealed class DefaultModuleLoader : IModuleLoader
         }
 
         return new ResolvedSpecifier(
-            specifier,
+            moduleRequest,
             resolved.AbsoluteUri,
             resolved,
             SpecifierType.RelativeOrAbsolute
         );
     }
 
-    public Module LoadModule(Engine engine, ResolvedSpecifier resolved)
+    protected override string LoadModuleContents(Engine engine, ResolvedSpecifier resolved)
     {
+        var specifier = resolved.ModuleRequest.Specifier;
         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;
+            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: '{specifier}'.");
         }
 
         if (resolved.Uri == null)
         {
-            ExceptionHelper.ThrowInvalidOperationException($"Module '{resolved.Specifier}' of type '{resolved.Type}' has no resolved URI.");
+            ExceptionHelper.ThrowInvalidOperationException($"Module '{specifier}' of type '{resolved.Type}' has no resolved URI.");
         }
 
         var fileName = Uri.UnescapeDataString(resolved.Uri.AbsolutePath);
         if (!File.Exists(fileName))
         {
-            ExceptionHelper.ThrowModuleResolutionException("Module Not Found", resolved.Specifier, parent: null, fileName);
-            return default;
-        }
-
-        var code = File.ReadAllText(fileName);
-
-        var source = resolved.Uri.LocalPath;
-        Module module;
-        try
-        {
-            module = new JavaScriptParser().ParseModule(code, source);
-        }
-        catch (ParserException ex)
-        {
-            ExceptionHelper.ThrowSyntaxError(engine.Realm, $"Error while loading module: error in module '{source}': {ex.Error}");
-            module = null;
-        }
-        catch (Exception)
-        {
-            ExceptionHelper.ThrowJavaScriptException(engine, $"Could not load module {source}", (Location) default);
-            module = null;
+            ExceptionHelper.ThrowModuleResolutionException("Module Not Found", specifier, parent: null, fileName);
         }
 
-        return module;
+        return File.ReadAllText(fileName);
     }
 
     private static bool IsRelative(string specifier)

+ 3 - 5
Jint/Runtime/Modules/FailFastModuleLoader.cs

@@ -1,5 +1,3 @@
-using Esprima.Ast;
-
 namespace Jint.Runtime.Modules;
 
 internal sealed class FailFastModuleLoader : IModuleLoader
@@ -17,12 +15,12 @@ internal sealed class FailFastModuleLoader : IModuleLoader
         }
     }
 
-    public ResolvedSpecifier Resolve(string? referencingModuleLocation, string specifier)
+    public ResolvedSpecifier Resolve(string? referencingModuleLocation, ModuleRequest moduleRequest)
     {
-        return new ResolvedSpecifier(specifier, specifier, null, SpecifierType.Bare);
+        return new ResolvedSpecifier(moduleRequest, moduleRequest.Specifier, Uri: null, SpecifierType.Bare);
     }
 
-    public Module LoadModule(Engine engine, ResolvedSpecifier resolved)
+    public ModuleRecord LoadModule(Engine engine, ResolvedSpecifier resolved)
     {
         ThrowDisabledException();
         return default!;

+ 2 - 4
Jint/Runtime/Modules/IModuleLoader.cs

@@ -1,5 +1,3 @@
-using Esprima.Ast;
-
 namespace Jint.Runtime.Modules;
 
 /// <summary>
@@ -10,10 +8,10 @@ public interface IModuleLoader
     /// <summary>
     /// Resolves a specifier to a path or module
     /// </summary>
-    ResolvedSpecifier Resolve(string? referencingModuleLocation, string specifier);
+    ResolvedSpecifier Resolve(string? referencingModuleLocation, ModuleRequest moduleRequest);
 
     /// <summary>
     /// Loads a module from given location.
     /// </summary>
-    public Module LoadModule(Engine engine, ResolvedSpecifier resolved);
+    public ModuleRecord LoadModule(Engine engine, ResolvedSpecifier resolved);
 }

+ 77 - 0
Jint/Runtime/Modules/ModuleLoader.cs

@@ -0,0 +1,77 @@
+using Esprima;
+using Esprima.Ast;
+using Jint.Native;
+using Jint.Native.Json;
+
+namespace Jint.Runtime.Modules;
+
+/// <summary>
+/// Base template for module loaders.
+/// </summary>
+public abstract class ModuleLoader : IModuleLoader
+{
+    public abstract ResolvedSpecifier Resolve(string? referencingModuleLocation, ModuleRequest moduleRequest);
+
+    public ModuleRecord LoadModule(Engine engine, ResolvedSpecifier resolved)
+    {
+        string code;
+        try
+        {
+            code = LoadModuleContents(engine, resolved);
+        }
+        catch (Exception)
+        {
+            ExceptionHelper.ThrowJavaScriptException(engine, $"Could not load module {resolved.ModuleRequest.Specifier}", (Location) default);
+            return default!;
+        }
+
+        var isJson = resolved.ModuleRequest.Attributes != null
+                     && Array.Exists(resolved.ModuleRequest.Attributes, x => string.Equals(x.Key, "type", StringComparison.Ordinal) && string.Equals(x.Value, "json", StringComparison.Ordinal));
+
+        ModuleRecord moduleRecord = isJson
+            ? BuildJsonModule(engine, resolved, code)
+            : BuildSourceTextModule(engine, resolved, code);
+
+        return moduleRecord;
+    }
+
+    protected abstract string LoadModuleContents(Engine engine, ResolvedSpecifier resolved);
+
+    private static SyntheticModuleRecord BuildJsonModule(Engine engine, ResolvedSpecifier resolved, string code)
+    {
+        var source = resolved.Uri?.LocalPath;
+        JsValue module;
+        try
+        {
+            module = new JsonParser(engine).Parse(code);
+        }
+        catch (Exception)
+        {
+            ExceptionHelper.ThrowJavaScriptException(engine, $"Could not load module {source}", (Location) default);
+            module = null;
+        }
+
+        return new SyntheticModuleRecord(engine, engine.Realm, module, resolved.Uri?.LocalPath);
+    }
+    private static SourceTextModuleRecord BuildSourceTextModule(Engine engine, ResolvedSpecifier resolved, string code)
+    {
+        var source = resolved.Uri?.LocalPath;
+        Module module;
+        try
+        {
+            module = new JavaScriptParser().ParseModule(code, source);
+        }
+        catch (ParserException ex)
+        {
+            ExceptionHelper.ThrowSyntaxError(engine.Realm, $"Error while loading module: error in module '{source}': {ex.Error}");
+            module = null;
+        }
+        catch (Exception)
+        {
+            ExceptionHelper.ThrowJavaScriptException(engine, $"Could not load module {source}", (Location) default);
+            module = null;
+        }
+
+        return new SourceTextModuleRecord(engine, engine.Realm, module, resolved.Uri?.LocalPath, async: false);
+    }
+}

+ 11 - 5
Jint/Runtime/Modules/ModuleRequest.cs

@@ -1,8 +1,8 @@
-using Jint.Native;
-
 namespace Jint.Runtime.Modules;
 
-internal readonly record struct ModuleRequest(string Specifier, List<KeyValuePair<string, JsValue>> Attributes)
+public readonly record struct ModuleImportAttribute(string Key, string Value);
+
+public readonly record struct ModuleRequest(string Specifier, ModuleImportAttribute[] Attributes)
 {
     /// <summary>
     /// https://tc39.es/proposal-import-attributes/#sec-ModuleRequestsEqual
@@ -14,14 +14,20 @@ internal readonly record struct ModuleRequest(string Specifier, List<KeyValuePai
             return false;
         }
 
-        if (this.Attributes.Count != other.Attributes.Count)
+        if (this.Attributes.Length != other.Attributes.Length)
         {
             return false;
         }
 
+        if (Attributes.Length == 0
+            || (Attributes.Length == 1 && Attributes[0].Equals(other.Attributes[0])))
+        {
+            return true;
+        }
+
         foreach (var pair in Attributes)
         {
-            if (!other.Attributes.Contains(pair))
+            if (Array.IndexOf(other.Attributes, pair) == -1)
             {
                 return false;
             }

+ 1 - 1
Jint/Runtime/Modules/ResolvedSpecifier.cs

@@ -1,3 +1,3 @@
 namespace Jint.Runtime.Modules;
 
-public record ResolvedSpecifier(string Specifier, string Key, Uri? Uri, SpecifierType Type);
+public record ResolvedSpecifier(ModuleRequest ModuleRequest, string Key, Uri? Uri, SpecifierType Type);

+ 18 - 30
Jint/Runtime/Modules/SourceTextModuleRecord.cs

@@ -1,6 +1,4 @@
-#nullable disable
-
-using Esprima.Ast;
+using Esprima.Ast;
 using Jint.Native.Object;
 using Jint.Native.Promise;
 using Jint.Runtime.Environments;
@@ -11,21 +9,12 @@ namespace Jint.Runtime.Modules;
 /// <summary>
 /// https://tc39.es/ecma262/#importentry-record
 /// </summary>
-internal sealed record ImportEntry(
-    string ModuleRequest,
-    string ImportName,
-    string LocalName
-);
+internal sealed record ImportEntry(ModuleRequest ModuleRequest, string? ImportName, string LocalName);
 
 /// <summary>
 /// https://tc39.es/ecma262/#exportentry-record
 /// </summary>
-internal sealed record ExportEntry(
-    string ExportName,
-    string ModuleRequest,
-    string ImportName,
-    string LocalName
-);
+internal sealed record ExportEntry(string? ExportName, ModuleRequest? ModuleRequest, string? ImportName, string? LocalName);
 
 /// <summary>
 /// https://tc39.es/ecma262/#sec-source-text-module-records
@@ -34,14 +23,14 @@ internal class SourceTextModuleRecord : CyclicModuleRecord
 {
     internal readonly Module _source;
     private ExecutionContext _context;
-    private ObjectInstance _importMeta;
-    private readonly List<ImportEntry> _importEntries;
+    private ObjectInstance? _importMeta;
+    private readonly List<ImportEntry>? _importEntries;
     internal readonly List<ExportEntry> _localExportEntries;
     private readonly List<ExportEntry> _indirectExportEntries;
     private readonly List<ExportEntry> _starExportEntries;
 
-    internal SourceTextModuleRecord(Engine engine, Realm realm, Module source, string location, bool async)
-        : base(engine, realm, source, location, async)
+    internal SourceTextModuleRecord(Engine engine, Realm realm, Module source, string? location, bool async)
+        : base(engine, realm, location, async)
     {
         _source = source;
 
@@ -75,17 +64,17 @@ internal class SourceTextModuleRecord : CyclicModuleRecord
     /// <summary>
     /// https://tc39.es/ecma262/#sec-getexportednames
     /// </summary>
-    public override List<string> GetExportedNames(List<CyclicModuleRecord> exportStarSet = null)
+    public override List<string?> GetExportedNames(List<CyclicModuleRecord>? exportStarSet = null)
     {
         exportStarSet ??= new List<CyclicModuleRecord>();
         if (exportStarSet.Contains(this))
         {
             //Reached the starting point of an export * circularity
-            return new List<string>();
+            return new List<string?>();
         }
 
         exportStarSet.Add(this);
-        var exportedNames = new List<string>();
+        var exportedNames = new List<string?>();
         for (var i = 0; i < _localExportEntries.Count; i++)
         {
             var e = _localExportEntries[i];
@@ -101,7 +90,7 @@ internal class SourceTextModuleRecord : CyclicModuleRecord
         for (var i = 0; i < _starExportEntries.Count; i++)
         {
             var e = _starExportEntries[i];
-            var requestedModule = _engine._host.GetImportedModule(this, e.ModuleRequest);
+            var requestedModule = _engine._host.GetImportedModule(this, e.ModuleRequest!.Value);
             var starNames = requestedModule.GetExportedNames(exportStarSet);
 
             for (var j = 0; j < starNames.Count; j++)
@@ -120,7 +109,7 @@ internal class SourceTextModuleRecord : CyclicModuleRecord
     /// <summary>
     /// https://tc39.es/ecma262/#sec-resolveexport
     /// </summary>
-    internal override ResolvedBinding ResolveExport(string exportName, List<ExportResolveSetItem> resolveSet = null)
+    internal override ResolvedBinding? ResolveExport(string? exportName, List<ExportResolveSetItem>? resolveSet = null)
     {
         resolveSet ??= new List<ExportResolveSetItem>();
 
@@ -150,7 +139,7 @@ internal class SourceTextModuleRecord : CyclicModuleRecord
             var e = _indirectExportEntries[i];
             if (string.Equals(exportName, e.ExportName, StringComparison.Ordinal))
             {
-                var importedModule = _engine._host.GetImportedModule(this, e.ModuleRequest);
+                var importedModule = _engine._host.GetImportedModule(this, e.ModuleRequest!.Value);
                 if (string.Equals(e.ImportName, "*", StringComparison.Ordinal))
                 {
                     // 1. Assert: module does not provide the direct binding for this export.
@@ -170,12 +159,12 @@ internal class SourceTextModuleRecord : CyclicModuleRecord
             return null;
         }
 
-        ResolvedBinding starResolution = null;
+        ResolvedBinding? starResolution = null;
 
         for (var i = 0; i < _starExportEntries.Count; i++)
         {
             var e = _starExportEntries[i];
-            var importedModule = _engine._host.GetImportedModule(this, e.ModuleRequest);
+            var importedModule = _engine._host.GetImportedModule(this, e.ModuleRequest!.Value);
             var resolution = importedModule.ResolveExport(exportName, resolveSet);
             if (resolution == ResolvedBinding.Ambiguous)
             {
@@ -317,8 +306,7 @@ internal class SourceTextModuleRecord : CyclicModuleRecord
                 var d = functionDeclarations[i];
                 var fn = d.Id?.Name ?? "*default*";
                 var fd = new JintFunctionDefinition(d);
-                env.CreateMutableBinding(fn, true);
-                // TODO private scope
+                env.CreateMutableBinding(fn, canBeDeleted: true);
                 var fo = realm.Intrinsics.Function.InstantiateFunctionObject(fd, env, privateEnv: null);
                 if (string.Equals(fn, "*default*", StringComparison.Ordinal))
                 {
@@ -334,12 +322,12 @@ internal class SourceTextModuleRecord : CyclicModuleRecord
     /// <summary>
     /// https://tc39.es/ecma262/#sec-source-text-module-record-execute-module
     /// </summary>
-    internal override Completion ExecuteModule(PromiseCapability capability = null)
+    internal override Completion ExecuteModule(PromiseCapability? capability = null)
     {
         var moduleContext = new ExecutionContext(this, _environment, _environment, privateEnvironment: null, _realm);
         if (!_hasTLA)
         {
-            using (new StrictModeScope(true, force: true))
+            using (new StrictModeScope(strict: true, force: true))
             {
                 _engine.EnterExecutionContext(moduleContext);
                 try

+ 76 - 0
Jint/Runtime/Modules/SyntheticModuleRecord.cs

@@ -0,0 +1,76 @@
+using Esprima.Ast;
+using Jint.Native;
+using Jint.Native.Promise;
+using Jint.Runtime.Environments;
+
+namespace Jint.Runtime.Modules;
+
+internal sealed class SyntheticModuleRecord : ModuleRecord
+{
+    private readonly JsValue _obj;
+    private readonly List<string> _exportNames = ["default"];
+
+    internal SyntheticModuleRecord(Engine engine, Realm realm, JsValue obj, string? location)
+        : base(engine, realm, location)
+    {
+        _obj = obj;
+
+        var env = JintEnvironment.NewModuleEnvironment(_engine, realm.GlobalEnv);
+        _environment = env;
+    }
+
+    public override List<string> GetExportedNames(List<CyclicModuleRecord>? exportStarSet = null) => _exportNames;
+
+    internal override ResolvedBinding? ResolveExport(string exportName, List<ExportResolveSetItem>? resolveSet = null)
+    {
+        if (!_exportNames.Contains(exportName))
+        {
+            return null;
+        }
+
+        return new ResolvedBinding(this, exportName);
+    }
+
+    public override void Link()
+    {
+    }
+
+    public override JsValue Evaluate()
+    {
+        var moduleContext = new ExecutionContext(
+            function: null,
+            realm: _realm,
+            scriptOrModule: this,
+            variableEnvironment: _environment,
+            lexicalEnvironment: _environment,
+            privateEnvironment: null,
+            generator: null);
+
+        // 7.Suspend the currently running execution context.
+        _engine.EnterExecutionContext(moduleContext);
+
+        _environment.SetMutableBinding("default", _obj, strict: true);
+
+        _engine.LeaveExecutionContext();
+
+        var pc = PromiseConstructor.NewPromiseCapability(_engine, _realm.Intrinsics.Promise);
+        pc.Resolve.Call(Undefined, Array.Empty<JsValue>());
+        return pc.PromiseInstance;
+    }
+
+    protected internal override int InnerModuleLinking(Stack<CyclicModuleRecord> stack, int index)
+    {
+        foreach (var exportName in _exportNames)
+        {
+            _environment.CreateMutableBinding(exportName, canBeDeleted: false);
+            _environment.InitializeBinding(exportName, Undefined);
+        }
+        return index;
+    }
+
+    protected internal override Completion InnerModuleEvaluation(Stack<CyclicModuleRecord> stack, int index, ref int asyncEvalOrder)
+    {
+        _environment.SetMutableBinding("default", _obj, strict: true);
+        return new Completion(CompletionType.Normal, index, new Identifier(""));
+    }
+}

+ 1 - 0
README.md

@@ -112,6 +112,7 @@ Following features are supported in version 3.x.
 - ✔ `ArrayBuffer.prototype.transfer`
 - ✔ Array Grouping - `Object.groupBy` and `Map.groupBy`
 - ✔ Import attributes
+- ✔ JSON modules
 - ✔ `Promise.withResolvers`
 - ✔ Resizable and growable ArrayBuffers
 - ✔ ShadowRealm