-
-
Notifications
You must be signed in to change notification settings - Fork 567
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Support ECMAScript modules (export/import statements, modules definit…
…ion) (#1054)
- Loading branch information
1 parent
e31a0a7
commit fcc7c8d
Showing
42 changed files
with
1,147 additions
and
181 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,249 @@ | ||
#if(NET6_0_OR_GREATER) | ||
using System.IO; | ||
using System.Reflection; | ||
#endif | ||
using System; | ||
using Jint.Native; | ||
using Jint.Runtime; | ||
using Xunit; | ||
|
||
namespace Jint.Tests.Runtime; | ||
|
||
public class ModuleTests | ||
{ | ||
private readonly Engine _engine; | ||
|
||
public ModuleTests() | ||
{ | ||
_engine = new Engine(); | ||
} | ||
|
||
[Fact] | ||
public void ShouldExportNamed() | ||
{ | ||
_engine.AddModule("my-module", @"export const value = 'exported value';"); | ||
var ns = _engine.ImportModule("my-module"); | ||
|
||
Assert.Equal("exported value", ns.Get("value").AsString()); | ||
} | ||
|
||
[Fact] | ||
public void ShouldExportNamedListRenamed() | ||
{ | ||
_engine.AddModule("my-module", @"const value1 = 1; const value2 = 2; export { value1 as renamed1, value2 as renamed2 }"); | ||
var ns = _engine.ImportModule("my-module"); | ||
|
||
Assert.Equal(1, ns.Get("renamed1").AsInteger()); | ||
Assert.Equal(2, ns.Get("renamed2").AsInteger()); | ||
} | ||
|
||
[Fact] | ||
public void ShouldExportDefault() | ||
{ | ||
_engine.AddModule("my-module", @"export default 'exported value';"); | ||
var ns = _engine.ImportModule("my-module"); | ||
|
||
Assert.Equal("exported value", ns.Get("default").AsString()); | ||
} | ||
|
||
[Fact] | ||
public void ShouldExportAll() | ||
{ | ||
_engine.AddModule("module1", @"export const value = 'exported value';"); | ||
_engine.AddModule("module2", @"export * from 'module1';"); | ||
var ns = _engine.ImportModule("module2"); | ||
|
||
Assert.Equal("exported value", ns.Get("value").AsString()); | ||
} | ||
|
||
[Fact] | ||
public void ShouldImportNamed() | ||
{ | ||
_engine.AddModule("imported-module", @"export const value = 'exported value';"); | ||
_engine.AddModule("my-module", @"import { value } from 'imported-module'; export const exported = value;"); | ||
var ns = _engine.ImportModule("my-module"); | ||
|
||
Assert.Equal("exported value", ns.Get("exported").AsString()); | ||
} | ||
|
||
[Fact] | ||
public void ShouldImportRenamed() | ||
{ | ||
_engine.AddModule("imported-module", @"export const value = 'exported value';"); | ||
_engine.AddModule("my-module", @"import { value as renamed } from 'imported-module'; export const exported = renamed;"); | ||
var ns = _engine.ImportModule("my-module"); | ||
|
||
Assert.Equal("exported value", ns.Get("exported").AsString()); | ||
} | ||
|
||
[Fact] | ||
public void ShouldImportDefault() | ||
{ | ||
_engine.AddModule("imported-module", @"export default 'exported value';"); | ||
_engine.AddModule("my-module", @"import imported from 'imported-module'; export const exported = imported;"); | ||
var ns = _engine.ImportModule("my-module"); | ||
|
||
Assert.Equal("exported value", ns.Get("exported").AsString()); | ||
} | ||
|
||
[Fact] | ||
public void ShouldImportAll() | ||
{ | ||
_engine.AddModule("imported-module", @"export const value = 'exported value';"); | ||
_engine.AddModule("my-module", @"import * as imported from 'imported-module'; export const exported = imported.value;"); | ||
var ns = _engine.ImportModule("my-module"); | ||
|
||
Assert.Equal("exported value", ns.Get("exported").AsString()); | ||
} | ||
|
||
[Fact] | ||
public void ShouldPropagateThrowStatementOnCSharpImport() | ||
{ | ||
_engine.AddModule("my-module", @"throw new Error('imported successfully');"); | ||
|
||
var exc = Assert.Throws<JavaScriptException>(() => _engine.ImportModule("my-module")); | ||
Assert.Equal("imported successfully", exc.Message); | ||
Assert.Equal("my-module", exc.Location.Source); | ||
} | ||
|
||
[Fact] | ||
public void ShouldPropagateThrowStatementThroughJavaScriptImport() | ||
{ | ||
_engine.AddModule("imported-module", @"throw new Error('imported successfully');"); | ||
_engine.AddModule("my-module", @"import 'imported-module';"); | ||
|
||
var exc = Assert.Throws<JavaScriptException>(() => _engine.ImportModule("my-module")); | ||
Assert.Equal("imported successfully", exc.Message); | ||
Assert.Equal("imported-module", exc.Location.Source); | ||
} | ||
|
||
[Fact] | ||
public void ShouldAddModuleFromJsValue() | ||
{ | ||
_engine.AddModule("my-module", builder => builder.ExportValue("value", JsString.Create("hello world"))); | ||
var ns = _engine.ImportModule("my-module"); | ||
|
||
Assert.Equal("hello world", ns.Get("value").AsString()); | ||
} | ||
|
||
[Fact] | ||
public void ShouldAddModuleFromClrInstance() | ||
{ | ||
_engine.AddModule("imported-module", builder => builder.ExportObject("value", new ImportedClass { Value = "instance value" })); | ||
_engine.AddModule("my-module", @"import { value } from 'imported-module'; export const exported = value.value;"); | ||
var ns = _engine.ImportModule("my-module"); | ||
|
||
Assert.Equal("instance value", ns.Get("exported").AsString()); | ||
} | ||
|
||
[Fact] | ||
public void ShouldAllowInvokeUserDefinedClass() | ||
{ | ||
_engine.AddModule("user", "export class UserDefined { constructor(v) { this._v = v; } hello(c) { return `hello ${this._v}${c}`; } }"); | ||
var ctor = _engine.ImportModule("user").Get("UserDefined"); | ||
var instance = _engine.Construct(ctor, JsString.Create("world")); | ||
var result = instance.GetMethod("hello").Call(instance, JsString.Create("!")); | ||
|
||
Assert.Equal("hello world!", result); | ||
} | ||
|
||
[Fact] | ||
public void ShouldAddModuleFromClrType() | ||
{ | ||
_engine.AddModule("imported-module", builder => builder.ExportType<ImportedClass>()); | ||
_engine.AddModule("my-module", @"import { ImportedClass } from 'imported-module'; export const exported = new ImportedClass().value;"); | ||
var ns = _engine.ImportModule("my-module"); | ||
|
||
Assert.Equal("hello world", ns.Get("exported").AsString()); | ||
} | ||
|
||
private class ImportedClass | ||
{ | ||
public string Value { get; set; } = "hello world"; | ||
} | ||
|
||
[Fact] | ||
public void ShouldAllowExportMultipleImports() | ||
{ | ||
_engine.AddModule("@mine/import1", builder => builder.ExportValue("value1", JsNumber.Create(1))); | ||
_engine.AddModule("@mine/import2", builder => builder.ExportValue("value2", JsNumber.Create(2))); | ||
_engine.AddModule("@mine", "export * from '@mine/import1'; export * from '@mine/import2'"); | ||
_engine.AddModule("app", @"import { value1, value2 } from '@mine'; export const result = `${value1} ${value2}`"); | ||
var ns = _engine.ImportModule("app"); | ||
|
||
Assert.Equal("1 2", ns.Get("result").AsString()); | ||
} | ||
|
||
/* ECMAScript 2020 "export * as ns from" | ||
[Fact] | ||
public void ShouldAllowNamedStarExport() | ||
{ | ||
_engine.AddModule("imported-module", builder => builder.ExportValue("value1", 5)); | ||
_engine.AddModule("my-module", "export * as ns from 'imported-module';"); | ||
var ns = _engine.ImportModule("my-module"); | ||
Assert.Equal(5, ns.Get("ns").Get("value1").AsNumber()); | ||
} | ||
*/ | ||
|
||
[Fact] | ||
public void ShouldAllowChaining() | ||
{ | ||
_engine.AddModule("dependent-module", "export const dependency = 1;"); | ||
_engine.AddModule("my-module", builder => builder | ||
.AddSource("import { dependency } from 'dependent-module';") | ||
.AddSource("export const output = dependency + 1;") | ||
.ExportValue("num", JsNumber.Create(-1)) | ||
); | ||
var ns = _engine.ImportModule("my-module"); | ||
|
||
Assert.Equal(2, ns.Get("output").AsInteger()); | ||
Assert.Equal(-1, ns.Get("num").AsInteger()); | ||
} | ||
|
||
[Fact] | ||
public void ShouldAllowLoadingMoreThanOnce() | ||
{ | ||
var called = 0; | ||
_engine.AddModule("imported-module", builder => builder.ExportFunction("count", args => called++)); | ||
_engine.AddModule("my-module", @"import { count } from 'imported-module'; count();"); | ||
_engine.ImportModule("my-module"); | ||
_engine.ImportModule("my-module"); | ||
|
||
Assert.Equal(called, 1); | ||
} | ||
|
||
#if(NET6_0_OR_GREATER) | ||
|
||
[Fact] | ||
public void CanLoadModuleImportsFromFiles() | ||
{ | ||
var engine = new Engine(options => options.EnableModules(GetBasePath())); | ||
engine.AddModule("my-module", "import { User } from './modules/user.js'; export const user = new User('John', 'Doe');"); | ||
var ns = engine.ImportModule("my-module"); | ||
|
||
Assert.Equal("John Doe", ns["user"].Get("name").AsString()); | ||
} | ||
|
||
[Fact] | ||
public void CanImportFromFile() | ||
{ | ||
var engine = new Engine(options => options.EnableModules(GetBasePath())); | ||
var ns = engine.ImportModule("./modules/format-name.js"); | ||
var result = engine.Invoke(ns.Get("formatName"), "John", "Doe").AsString(); | ||
|
||
Assert.Equal("John Doe", result); | ||
} | ||
|
||
private static string GetBasePath() | ||
{ | ||
var assemblyPath = new Uri(typeof(ModuleTests).GetTypeInfo().Assembly.Location).LocalPath; | ||
var assemblyDirectory = new FileInfo(assemblyPath).Directory; | ||
return Path.Combine( | ||
assemblyDirectory?.Parent?.Parent?.Parent?.FullName ?? throw new NullReferenceException("Could not find tests base path"), | ||
"Runtime", | ||
"Scripts"); | ||
} | ||
|
||
#endif | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
using Jint.Runtime.Modules; | ||
using Xunit; | ||
|
||
namespace Jint.Tests.Runtime.Modules; | ||
|
||
public class DefaultModuleLoaderTests | ||
{ | ||
[Theory] | ||
[InlineData("./other.js", @"file:///project/folder/other.js")] | ||
[InlineData("../model/other.js", @"file:///project/model/other.js")] | ||
[InlineData("/project/model/other.js", @"file:///project/model/other.js")] | ||
[InlineData("file:///project/model/other.js", @"file:///project/model/other.js")] | ||
public void ShouldResolveRelativePaths(string specifier, string expectedUri) | ||
{ | ||
var resolver = new DefaultModuleLoader("file:///project"); | ||
|
||
var resolved = resolver.Resolve("file:///project/folder/script.js", specifier); | ||
|
||
Assert.Equal(specifier, resolved.Specifier); | ||
Assert.Equal(expectedUri, resolved.Key); | ||
Assert.Equal(expectedUri, resolved.Uri?.AbsoluteUri); | ||
Assert.Equal(SpecifierType.RelativeOrAbsolute, resolved.Type); | ||
} | ||
|
||
[Theory] | ||
[InlineData("./../../other.js")] | ||
[InlineData("../../model/other.js")] | ||
[InlineData("/model/other.js")] | ||
[InlineData("file:///etc/secret.js")] | ||
public void ShouldRejectPathsOutsideOfBasePath(string specifier) | ||
{ | ||
var resolver = new DefaultModuleLoader("file:///project"); | ||
|
||
var exc = Assert.Throws<ModuleResolutionException>(() => resolver.Resolve("file:///project/folder/script.js", specifier)); | ||
Assert.StartsWith(exc.ResolverAlgorithmError, "Unauthorized Module Path"); | ||
Assert.StartsWith(exc.Specifier, specifier); | ||
} | ||
|
||
[Fact] | ||
public void ShouldResolveBareSpecifiers() | ||
{ | ||
var resolver = new DefaultModuleLoader("/"); | ||
|
||
var resolved = resolver.Resolve(null, "my-module"); | ||
|
||
Assert.Equal("my-module", resolved.Specifier); | ||
Assert.Equal("my-module", resolved.Key); | ||
Assert.Equal(null, resolved.Uri?.AbsoluteUri); | ||
Assert.Equal(SpecifierType.Bare, resolved.Type); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export function formatName(firstName, lastName) { | ||
return `${firstName} ${lastName}`; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 }; |
Oops, something went wrong.