Skip to content

Commit 0c223f4

Browse files
pluethi1lphlahmasebastienros
authored
Module loading support (#990)
Co-authored-by: lph <[email protected]> Co-authored-by: Marko Lahma <[email protected]> Co-authored-by: Sébastien Ros <[email protected]>
1 parent 9d84326 commit 0c223f4

22 files changed

+1860
-38
lines changed
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Reflection;
4+
using System.IO;
5+
using Jint.Native;
6+
using Jint.Native.Object;
7+
using Jint.Runtime;
8+
using Jint.Runtime.Interop;
9+
10+
namespace Jint.Tests.Test262.Language
11+
{
12+
// Hacky way to get objects from assert.js and sta.js into the module context
13+
internal sealed class ModuleTestHost : Host
14+
{
15+
private readonly static Dictionary<string, JsValue> _staticValues = new();
16+
17+
static ModuleTestHost()
18+
{
19+
var assemblyPath = new Uri(typeof(ModuleTestHost).GetTypeInfo().Assembly.Location).LocalPath;
20+
var assemblyDirectory = new FileInfo(assemblyPath).Directory;
21+
22+
var basePath = assemblyDirectory.Parent.Parent.Parent.FullName;
23+
24+
var engine = new Engine();
25+
var assertSource = File.ReadAllText(Path.Combine(basePath, "harness", "assert.js"));
26+
var staSource = File.ReadAllText(Path.Combine(basePath, "harness", "sta.js"));
27+
28+
engine.Execute(assertSource);
29+
engine.Execute(staSource);
30+
31+
_staticValues["assert"] = engine.GetValue("assert");
32+
_staticValues["Test262Error"] = engine.GetValue("Test262Error");
33+
_staticValues["$ERROR"] = engine.GetValue("$ERROR");
34+
_staticValues["$DONOTEVALUATE"] = engine.GetValue("$DONOTEVALUATE");
35+
36+
_staticValues["print"] = new ClrFunctionInstance(engine, "print", (thisObj, args) => TypeConverter.ToString(args.At(0)));
37+
}
38+
39+
protected override ObjectInstance CreateGlobalObject(Realm realm)
40+
{
41+
var globalObj = base.CreateGlobalObject(realm);
42+
43+
foreach (var key in _staticValues.Keys)
44+
{
45+
globalObj.FastAddProperty(key, _staticValues[key], true, true, true);
46+
}
47+
48+
return globalObj;
49+
}
50+
}
51+
}
Lines changed: 58 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,72 @@
1+
using Jint.Runtime;
2+
using Jint.Runtime.Modules;
3+
using System;
14
using Xunit;
5+
using Xunit.Sdk;
26

3-
namespace Jint.Tests.Test262.Language
7+
namespace Jint.Tests.Test262.Language;
8+
9+
public class ModuleTests : Test262Test
410
{
5-
public class ModuleTests : Test262Test
11+
[Theory(DisplayName = "language\\module-code")]
12+
[MemberData(nameof(SourceFiles), "language\\module-code", false)]
13+
[MemberData(nameof(SourceFiles), "language\\module-code", true, Skip = "Skipped")]
14+
protected void ModuleCode(SourceFile sourceFile)
15+
{
16+
RunModuleTest(sourceFile);
17+
}
18+
19+
[Theory(DisplayName = "language\\export")]
20+
[MemberData(nameof(SourceFiles), "language\\export", false)]
21+
[MemberData(nameof(SourceFiles), "language\\export", true, Skip = "Skipped")]
22+
protected void Export(SourceFile sourceFile)
23+
{
24+
RunModuleTest(sourceFile);
25+
}
26+
27+
[Theory(DisplayName = "language\\import")]
28+
[MemberData(nameof(SourceFiles), "language\\import", false)]
29+
[MemberData(nameof(SourceFiles), "language\\import", true, Skip = "Skipped")]
30+
protected void Import(SourceFile sourceFile)
31+
{
32+
RunModuleTest(sourceFile);
33+
}
34+
35+
private static void RunModuleTest(SourceFile sourceFile)
636
{
7-
[Theory(DisplayName = "language\\module-code", Skip = "TODO")]
8-
[MemberData(nameof(SourceFiles), "language\\module-code", false)]
9-
[MemberData(nameof(SourceFiles), "language\\module-code", true, Skip = "Skipped")]
10-
protected void ModuleCode(SourceFile sourceFile)
37+
if (sourceFile.Skip)
1138
{
12-
RunTestInternal(sourceFile);
39+
return;
1340
}
1441

15-
[Theory(DisplayName = "language\\export")]
16-
[MemberData(nameof(SourceFiles), "language\\export", false)]
17-
[MemberData(nameof(SourceFiles), "language\\export", true, Skip = "Skipped")]
18-
protected void Export(SourceFile sourceFile)
42+
var code = sourceFile.Code;
43+
44+
var options = new Options();
45+
options.Host.Factory = _ => new ModuleTestHost();
46+
options.Modules.Enabled = true;
47+
options.WithModuleLoader(new DefaultModuleLoader(null));
48+
49+
var engine = new Engine(options);
50+
51+
var negative = code.IndexOf("negative:", StringComparison.OrdinalIgnoreCase) != -1;
52+
string lastError = null;
53+
54+
try
55+
{
56+
engine.LoadModule(sourceFile.FullPath);
57+
}
58+
catch (JavaScriptException ex)
59+
{
60+
lastError = ex.ToString();
61+
}
62+
catch (Exception ex)
1963
{
20-
RunTestInternal(sourceFile);
64+
lastError = ex.ToString();
2165
}
2266

23-
[Theory(DisplayName = "language\\import", Skip = "TODO")]
24-
[MemberData(nameof(SourceFiles), "language\\import", false)]
25-
[MemberData(nameof(SourceFiles), "language\\import", true, Skip = "Skipped")]
26-
protected void Import(SourceFile sourceFile)
67+
if (!negative && !string.IsNullOrWhiteSpace(lastError))
2768
{
28-
RunTestInternal(sourceFile);
69+
throw new XunitException(lastError);
2970
}
3071
}
3172
}

Jint.Tests.Test262/Test262Test.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,13 @@ public static IEnumerable<object[]> SourceFiles(string pathPrefix, bool skipped)
208208

209209
foreach (var file in files)
210210
{
211+
if (file.IndexOf("_FIXTURE", StringComparison.OrdinalIgnoreCase) != -1)
212+
{
213+
// Files bearing a name which includes the sequence _FIXTURE MUST NOT be interpreted
214+
// as standalone tests; they are intended to be referenced by test files.
215+
continue;
216+
}
217+
211218
var name = file.Substring(fixturesPath.Length + 1).Replace("\\", "/");
212219
bool skip = _skipReasons.TryGetValue(name, out var reason);
213220

@@ -288,6 +295,18 @@ public static IEnumerable<object[]> SourceFiles(string pathPrefix, bool skipped)
288295
skip = true;
289296
reason = "resizable-arraybuffer not implemented";
290297
break;
298+
case "json-modules":
299+
skip = true;
300+
reason = "json-modules not implemented";
301+
break;
302+
case "top-level-await":
303+
skip = true;
304+
reason = "top-level-await not implemented";
305+
break;
306+
case "import-assertions":
307+
skip = true;
308+
reason = "import-assertions not implemented";
309+
break;
291310
}
292311
}
293312
}

Jint/Engine.Modules.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
using System.Collections.Generic;
2+
using Jint.Runtime.Modules;
3+
4+
namespace Jint
5+
{
6+
public partial class Engine
7+
{
8+
internal IModuleLoader ModuleLoader { get; set; }
9+
10+
private readonly Dictionary<ModuleCacheKey, JsModule> _modules = new();
11+
12+
public JsModule LoadModule(string specifier) => LoadModule(null, specifier);
13+
14+
internal JsModule LoadModule(string referencingModuleLocation, string specifier)
15+
{
16+
var key = new ModuleCacheKey(referencingModuleLocation ?? string.Empty, specifier);
17+
18+
if (_modules.TryGetValue(key, out var module))
19+
{
20+
return module;
21+
}
22+
23+
var (loadedModule, location) = ModuleLoader.LoadModule(this, specifier, referencingModuleLocation);
24+
module = new JsModule(this, _host.CreateRealm(), loadedModule, location.AbsoluteUri, false);
25+
26+
_modules[key] = module;
27+
28+
return module;
29+
}
30+
31+
internal readonly record struct ModuleCacheKey(string ReferencingModuleLocation, string Specifier);
32+
}
33+
}

Jint/EsprimaExtensions.cs

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
using Jint.Runtime.Environments;
1212
using Jint.Runtime.Interpreter;
1313
using Jint.Runtime.Interpreter.Expressions;
14+
using Jint.Runtime.Modules;
1415

1516
namespace Jint
1617
{
@@ -128,6 +129,7 @@ internal static string LiteralKeyToString(Literal literal)
128129
{
129130
return TypeConverter.ToString(d);
130131
}
132+
131133
return literal.Value as string ?? Convert.ToString(literal.Value, provider: null);
132134
}
133135

@@ -204,6 +206,7 @@ internal static void GetBoundNames(this Node? parameter, List<string> target)
204206
parameter = assignmentPattern.Left;
205207
continue;
206208
}
209+
207210
break;
208211
}
209212
}
@@ -256,6 +259,130 @@ internal static Record DefineMethod(this ClassProperty m, ObjectInstance obj, Ob
256259
return new Record(property, closure);
257260
}
258261

262+
internal static void GetImportEntries(this ImportDeclaration import, List<ImportEntry> importEntries, HashSet<string> requestedModules)
263+
{
264+
var source = import.Source.StringValue!;
265+
var specifiers = import.Specifiers;
266+
requestedModules.Add(source!);
267+
268+
foreach (var specifier in specifiers)
269+
{
270+
switch (specifier)
271+
{
272+
case ImportNamespaceSpecifier namespaceSpecifier:
273+
importEntries.Add(new ImportEntry(source, "*", namespaceSpecifier.Local.GetModuleKey()));
274+
break;
275+
case ImportSpecifier importSpecifier:
276+
importEntries.Add(new ImportEntry(source, importSpecifier.Imported.GetModuleKey(), importSpecifier.Local.GetModuleKey()));
277+
break;
278+
case ImportDefaultSpecifier defaultSpecifier:
279+
importEntries.Add(new ImportEntry(source, "default", defaultSpecifier.Local.GetModuleKey()));
280+
break;
281+
}
282+
}
283+
}
284+
285+
internal static void GetExportEntries(this ExportDeclaration export, List<ExportEntry> exportEntries, HashSet<string> requestedModules)
286+
{
287+
switch (export)
288+
{
289+
case ExportDefaultDeclaration defaultDeclaration:
290+
GetExportEntries(true, defaultDeclaration.Declaration, exportEntries);
291+
break;
292+
case ExportAllDeclaration allDeclaration:
293+
//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"'
294+
requestedModules.Add(allDeclaration.Source.StringValue!);
295+
exportEntries.Add(new(null, allDeclaration.Source.StringValue, "*", null));
296+
break;
297+
case ExportNamedDeclaration namedDeclaration:
298+
var specifiers = namedDeclaration.Specifiers;
299+
if (specifiers.Count == 0)
300+
{
301+
GetExportEntries(false, namedDeclaration.Declaration!, exportEntries, namedDeclaration.Source?.StringValue);
302+
303+
if (namedDeclaration.Source is not null)
304+
{
305+
requestedModules.Add(namedDeclaration.Source.StringValue!);
306+
}
307+
}
308+
else
309+
{
310+
foreach (var specifier in specifiers)
311+
{
312+
exportEntries.Add(new(specifier.Local.GetModuleKey(), namedDeclaration.Source?.StringValue, specifier.Exported.GetModuleKey(), null));
313+
}
314+
}
315+
316+
break;
317+
}
318+
}
319+
320+
private static void GetExportEntries(bool defaultExport, StatementListItem declaration, List<ExportEntry> exportEntries, string? moduleRequest = null)
321+
{
322+
var names = GetExportNames(declaration);
323+
324+
if (names.Count == 0)
325+
{
326+
if (defaultExport)
327+
{
328+
exportEntries.Add(new("default", null, null, "*default*"));
329+
}
330+
}
331+
else
332+
{
333+
for (var i = 0; i < names.Count; i++)
334+
{
335+
var name = names[i];
336+
var exportName = defaultExport ? "default" : name;
337+
exportEntries.Add(new(exportName, moduleRequest, null, name));
338+
}
339+
}
340+
}
341+
342+
private static List<string> GetExportNames(StatementListItem declaration)
343+
{
344+
var result = new List<string>();
345+
346+
switch (declaration)
347+
{
348+
case FunctionDeclaration functionDeclaration:
349+
var funcName = functionDeclaration.Id?.Name;
350+
if (funcName is not null)
351+
{
352+
result.Add(funcName);
353+
}
354+
355+
break;
356+
case ClassDeclaration classDeclaration:
357+
var className = classDeclaration.Id?.Name;
358+
if (className is not null)
359+
{
360+
result.Add(className);
361+
}
362+
363+
break;
364+
case VariableDeclaration variableDeclaration:
365+
var declarators = variableDeclaration.Declarations;
366+
foreach (var declarator in declarators)
367+
{
368+
var varName = declarator.Id.As<Identifier>()?.Name;
369+
if (varName is not null)
370+
{
371+
result.Add(varName);
372+
}
373+
}
374+
375+
break;
376+
}
377+
378+
return result;
379+
}
380+
381+
private static string? GetModuleKey(this Expression expression)
382+
{
383+
return (expression as Identifier)?.Name ?? (expression as Literal)?.StringValue;
384+
}
385+
259386
internal readonly record struct Record(JsValue Key, ScriptFunctionInstance Closure);
260387
}
261388
}

0 commit comments

Comments
 (0)