123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666 |
- using Jint.Native;
- using Jint.Runtime;
- using Jint.Runtime.Modules;
- using Module = Jint.Runtime.Modules.Module;
- namespace Jint.Tests.Runtime;
- public class ModuleTests
- {
- private readonly Engine _engine;
- public ModuleTests()
- {
- _engine = new Engine();
- }
- [Fact]
- public void ShouldExportNamed()
- {
- _engine.Modules.Add("my-module", "export const value = 'exported value';");
- var ns = _engine.Modules.Import("my-module");
- Assert.Equal("exported value", ns.Get("value").AsString());
- }
- [Fact]
- public void ShouldExportNamedListRenamed()
- {
- _engine.Modules.Add("my-module", "const value1 = 1; const value2 = 2; export { value1 as renamed1, value2 as renamed2 }");
- var ns = _engine.Modules.Import("my-module");
- Assert.Equal(1, ns.Get("renamed1").AsInteger());
- Assert.Equal(2, ns.Get("renamed2").AsInteger());
- }
- [Fact]
- public void ShouldExportDefault()
- {
- _engine.Modules.Add("my-module", "export default 'exported value';");
- var ns = _engine.Modules.Import("my-module");
- Assert.Equal("exported value", ns.Get("default").AsString());
- }
- [Fact]
- public void ShouldExportAll()
- {
- _engine.Modules.Add("module1", "export const value = 'exported value';");
- _engine.Modules.Add("module2", "export * from 'module1';");
- var ns = _engine.Modules.Import("module2");
- Assert.Equal("exported value", ns.Get("value").AsString());
- }
- [Fact]
- public void ShouldImportNamed()
- {
- _engine.Modules.Add("imported-module", "export const value = 'exported value';");
- _engine.Modules.Add("my-module", "import { value } from 'imported-module'; export const exported = value;");
- var ns = _engine.Modules.Import("my-module");
- Assert.Equal("exported value", ns.Get("exported").AsString());
- }
- [Fact]
- public void ShouldImportRenamed()
- {
- _engine.Modules.Add("imported-module", "export const value = 'exported value';");
- _engine.Modules.Add("my-module", "import { value as renamed } from 'imported-module'; export const exported = renamed;");
- var ns = _engine.Modules.Import("my-module");
- Assert.Equal("exported value", ns.Get("exported").AsString());
- }
- [Fact]
- public void ShouldImportDefault()
- {
- _engine.Modules.Add("imported-module", "export default 'exported value';");
- _engine.Modules.Add("my-module", "import imported from 'imported-module'; export const exported = imported;");
- var ns = _engine.Modules.Import("my-module");
- Assert.Equal("exported value", ns.Get("exported").AsString());
- }
- [Fact]
- public void ShouldImportAll()
- {
- _engine.Modules.Add("imported-module", "export const value = 'exported value';");
- _engine.Modules.Add("my-module", "import * as imported from 'imported-module'; export const exported = imported.value;");
- var ns = _engine.Modules.Import("my-module");
- Assert.Equal("exported value", ns.Get("exported").AsString());
- }
- [Fact]
- public void ShouldImportDynamically()
- {
- var received = false;
- _engine.Modules.Add("imported-module", builder => builder.ExportFunction("signal", () => received = true));
- _engine.Modules.Add("my-module", "import('imported-module').then(ns => { ns.signal(); });");
- _engine.Modules.Import("my-module");
- Assert.True(received);
- }
- [Fact]
- public void ShouldPropagateParseError()
- {
- _engine.Modules.Add("imported", "export const invalid;");
- _engine.Modules.Add("my-module", "import { invalid } from 'imported';");
- var exc = Assert.Throws<JavaScriptException>(() => _engine.Modules.Import("my-module"));
- Assert.Equal("Error while loading module: error in module 'imported': Line 1: Missing initializer in const declaration", exc.Message);
- Assert.Equal("imported", exc.Location.Source);
- }
- [Fact]
- public void ShouldPropagateLinkError()
- {
- _engine.Modules.Add("imported", "export invalid;");
- _engine.Modules.Add("my-module", "import { value } from 'imported';");
- var exc = Assert.Throws<JavaScriptException>(() => _engine.Modules.Import("my-module"));
- Assert.Equal("Error while loading module: error in module 'imported': Line 1: Unexpected identifier", exc.Message);
- Assert.Equal("imported", exc.Location.Source);
- }
- [Fact]
- public void ShouldPropagateExecuteError()
- {
- _engine.Modules.Add("my-module", "throw new Error('imported successfully');");
- var exc = Assert.Throws<JavaScriptException>(() => _engine.Modules.Import("my-module"));
- Assert.Equal("imported successfully", exc.Message);
- Assert.Equal("my-module", exc.Location.Source);
- }
- [Fact]
- public void ShouldPropagateThrowStatementThroughJavaScriptImport()
- {
- _engine.Modules.Add("imported-module", "throw new Error('imported successfully');");
- _engine.Modules.Add("my-module", "import 'imported-module';");
- var exc = Assert.Throws<JavaScriptException>(() => _engine.Modules.Import("my-module"));
- Assert.Equal("imported successfully", exc.Message);
- }
- [Fact]
- public void ShouldAddModuleFromJsValue()
- {
- _engine.Modules.Add("my-module", builder => builder.ExportValue("value", JsString.Create("hello world")));
- var ns = _engine.Modules.Import("my-module");
- Assert.Equal("hello world", ns.Get("value").AsString());
- }
- [Fact]
- public void ShouldAddModuleFromClrInstance()
- {
- _engine.Modules.Add("imported-module", builder => builder.ExportObject("value", new ImportedClass
- {
- Value = "instance value"
- }));
- _engine.Modules.Add("my-module", "import { value } from 'imported-module'; export const exported = value.value;");
- var ns = _engine.Modules.Import("my-module");
- Assert.Equal("instance value", ns.Get("exported").AsString());
- }
- [Fact]
- public void ShouldAllowInvokeUserDefinedClass()
- {
- _engine.Modules.Add("user", "export class UserDefined { constructor(v) { this._v = v; } hello(c) { return `hello ${this._v}${c}`; } }");
- var ctor = _engine.Modules.Import("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.Modules.Add("imported-module", builder => builder.ExportType<ImportedClass>());
- _engine.Modules.Add("my-module", "import { ImportedClass } from 'imported-module'; export const exported = new ImportedClass().value;");
- var ns = _engine.Modules.Import("my-module");
- Assert.Equal("hello world", ns.Get("exported").AsString());
- }
- [Fact]
- public void ShouldAddModuleFromClrFunction()
- {
- var received = new List<string>();
- _engine.Modules.Add("imported-module", builder => builder
- .ExportFunction("act_noargs", () => received.Add("act_noargs"))
- .ExportFunction("act_args", args => received.Add($"act_args:{args[0].AsString()}"))
- .ExportFunction("fn_noargs", () =>
- {
- received.Add("fn_noargs");
- return "ret";
- })
- .ExportFunction("fn_args", args =>
- {
- received.Add($"fn_args:{args[0].AsString()}");
- return "ret";
- })
- );
- _engine.Modules.Add("my-module", @"
- import * as fns from 'imported-module';
- export const result = [fns.act_noargs(), fns.act_args('ok'), fns.fn_noargs(), fns.fn_args('ok')];");
- var ns = _engine.Modules.Import("my-module");
- Assert.Equal(new[]
- {
- "act_noargs",
- "act_args:ok",
- "fn_noargs",
- "fn_args:ok"
- }, received.ToArray());
- Assert.Equal(new[]
- {
- "undefined",
- "undefined",
- "ret",
- "ret"
- }, ns.Get("result").AsArray().Select(x => x.ToString()).ToArray());
- }
- private class ImportedClass
- {
- public string Value { get; set; } = "hello world";
- }
- [Fact]
- public void ShouldAllowExportMultipleImports()
- {
- _engine.Modules.Add("@mine/import1", builder => builder.ExportValue("value1", JsNumber.Create(1)));
- _engine.Modules.Add("@mine/import2", builder => builder.ExportValue("value2", JsNumber.Create(2)));
- _engine.Modules.Add("@mine", "export * from '@mine/import1'; export * from '@mine/import2'");
- _engine.Modules.Add("app", "import { value1, value2 } from '@mine'; export const result = `${value1} ${value2}`");
- var ns = _engine.Modules.Import("app");
- Assert.Equal("1 2", ns.Get("result").AsString());
- }
- [Fact]
- public void ShouldAllowNamedStarExport()
- {
- _engine.Modules.Add("imported-module", builder => builder.ExportValue("value1", 5));
- _engine.Modules.Add("my-module", "export * as ns from 'imported-module';");
- var ns = _engine.Modules.Import("my-module");
- Assert.Equal(5, ns.Get("ns").Get("value1").AsNumber());
- }
- [Fact]
- public void ShouldAllowChaining()
- {
- _engine.Modules.Add("dependent-module", "export const dependency = 1;");
- _engine.Modules.Add("my-module", builder => builder
- .AddSource("import { dependency } from 'dependent-module';")
- .AddSource("export const output = dependency + 1;")
- .ExportValue("num", JsNumber.Create(-1))
- );
- var ns = _engine.Modules.Import("my-module");
- Assert.Equal(2, ns.Get("output").AsInteger());
- Assert.Equal(-1, ns.Get("num").AsInteger());
- }
- [Fact]
- public void ShouldImportOnlyOnce()
- {
- var called = 0;
- _engine.Modules.Add("imported-module", builder => builder.ExportFunction("count", args => called++));
- _engine.Modules.Add("my-module", "import { count } from 'imported-module'; count();");
- _engine.Modules.Import("my-module");
- _engine.Modules.Import("my-module");
- Assert.Equal(1, called);
- }
- [Fact]
- public void ShouldAllowSelfImport()
- {
- _engine.Modules.Add("my-globals", "export const globals = { counter: 0 };");
- _engine.Modules.Add("my-module", @"
- import { globals } from 'my-globals';
- import {} from 'my-module';
- globals.counter++;
- export const count = globals.counter;
- ");
- var ns = _engine.Modules.Import("my-module");
- Assert.Equal(1, ns.Get("count").AsInteger());
- }
- [Fact]
- public void ShouldAllowCyclicImport()
- {
- // https://tc39.es/ecma262/#sec-example-cyclic-module-record-graphs
- _engine.Modules.Add("B", "import { a } from 'A'; export const b = 'b';");
- _engine.Modules.Add("A", "import { b } from 'B'; export const a = 'a';");
- var nsA = _engine.Modules.Import("A");
- var nsB = _engine.Modules.Import("B");
- Assert.Equal("a", nsA.Get("a").AsString());
- Assert.Equal("b", nsB.Get("b").AsString());
- }
- [Fact]
- public void ShouldSupportConstraints()
- {
- var engine = new Engine(opts => opts.TimeoutInterval(TimeSpan.FromTicks(1)));
- engine.Modules.Add("sleep", builder => builder.ExportFunction("sleep", () => Thread.Sleep(100)));
- engine.Modules.Add("my-module", "import { sleep } from 'sleep'; for(var i = 0; i < 100; i++) { sleep(); } export const result = 'ok';");
- Assert.Throws<TimeoutException>(() => engine.Modules.Import("my-module"));
- }
- [Fact]
- public void CanLoadModuleImportsFromFiles()
- {
- var engine = new Engine(options => options.EnableModules(GetBasePath()));
- engine.Modules.Add("my-module", "import { User } from './modules/user.js'; export const user = new User('John', 'Doe');");
- var ns = engine.Modules.Import("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.Modules.Import("./modules/format-name.js");
- var result = engine.Invoke(ns.Get("formatName"), "John", "Doe").AsString();
- Assert.Equal("John Doe", result);
- }
- [Fact]
- public void CanImportFromFileWithSpacesInPath()
- {
- var engine = new Engine(options => options.EnableModules(GetBasePath()));
- var ns = engine.Modules.Import("./dir with spaces/format name.js");
- var result = engine.Invoke(ns.Get("formatName"), "John", "Doe").AsString();
- Assert.Equal("John Doe", result);
- }
- [Fact]
- public void CanReuseModule()
- {
- const string Code = "export function formatName(firstName, lastName) {\r\n return `${firstName} ${lastName}`;\r\n}";
- var module = Engine.PrepareModule(Code);
- for (var i = 0; i < 5; i++)
- {
- var engine = new Engine();
- engine.Modules.Add("__main__", x => x.AddModule(module));
- var ns = engine.Modules.Import("__main__");
- var result = engine.Invoke(ns.Get("formatName"), "John" + i, "Doe").AsString();
- Assert.Equal($"John{i} Doe", result);
- }
- }
- [Fact]
- public void EngineExecutePassesSourceForModuleResolving()
- {
- var moduleLoader = new EnforceRelativeModuleLoader(new Dictionary<string, string>()
- {
- ["file:///folder/my-module.js"] = "export const value = 'myModuleConst'"
- });
- var engine = new Engine(options => options.EnableModules(moduleLoader));
- var code = @"
- (async () => {
- const { value } = await import('./my-module.js');
- log(value);
- })();
- ";
- List<string> logStatements = new List<string>();
- engine.SetValue("log", logStatements.Add);
- engine.Execute(code, source: "file:///folder/main.js");
- engine.Advanced.ProcessTasks();
- Assert.Collection(
- logStatements,
- s => Assert.Equal("myModuleConst", s));
- }
- [Fact]
- public void EngineExecuteUsesScriptSourceForSource()
- {
- var moduleLoader = new EnforceRelativeModuleLoader(new Dictionary<string, string>()
- {
- ["file:///folder/my-module.js"] = "export const value = 'myModuleConst'"
- });
- var engine = new Engine(options => options.EnableModules(moduleLoader));
- var code = @"
- (async () => {
- const { value } = await import('./my-module.js');
- log(value);
- })();
- ";
- List<string> logStatements = new List<string>();
- engine.SetValue("log", logStatements.Add);
- var script = Engine.PrepareScript(code, source: "file:///folder/main.js");
- engine.Execute(script);
- engine.Advanced.ProcessTasks();
- Assert.Collection(
- logStatements,
- s => Assert.Equal("myModuleConst", s));
- }
- [Fact]
- public void EngineEvaluatePassesSourceForModuleResolving()
- {
- var moduleLoader = new EnforceRelativeModuleLoader(new Dictionary<string, string>()
- {
- ["file:///folder/my-module.js"] = "export const value = 'myModuleConst'"
- });
- var engine = new Engine(options => options.EnableModules(moduleLoader));
- var code = @"
- (async () => {
- const { value } = await import('./my-module.js');
- log(value);
- })();
- ";
- List<string> logStatements = new List<string>();
- engine.SetValue("log", logStatements.Add);
- engine.Evaluate(code, source: "file:///folder/main.js");
- engine.Advanced.ProcessTasks();
- Assert.Collection(
- logStatements,
- s => Assert.Equal("myModuleConst", s));
- }
- [Fact]
- public void EngineEvaluateUsesScriptSourceForSource()
- {
- var moduleLoader = new EnforceRelativeModuleLoader(new Dictionary<string, string>()
- {
- ["file:///folder/my-module.js"] = "export const value = 'myModuleConst'"
- });
- var engine = new Engine(options => options.EnableModules(moduleLoader));
- var code = @"
- (async () => {
- const { value } = await import('./my-module.js');
- log(value);
- })();
- ";
- List<string> logStatements = new List<string>();
- engine.SetValue("log", logStatements.Add);
- var script = Engine.PrepareScript(code, source: "file:///folder/main.js");
- engine.Evaluate(script);
- engine.Advanced.ProcessTasks();
- Assert.Collection(
- logStatements,
- s => Assert.Equal("myModuleConst", s));
- }
- private sealed class EnforceRelativeModuleLoader : IModuleLoader
- {
- private readonly IReadOnlyDictionary<string, string> _modules;
- public EnforceRelativeModuleLoader(IReadOnlyDictionary<string, string> modules)
- {
- _modules = modules;
- }
- public ResolvedSpecifier Resolve(string referencingModuleLocation, ModuleRequest moduleRequest)
- {
- Assert.False(string.IsNullOrEmpty(referencingModuleLocation), "Referencing module location is null or empty");
- var target = new Uri(new Uri(referencingModuleLocation, UriKind.Absolute), moduleRequest.Specifier);
- Assert.True(_modules.ContainsKey(target.ToString()), $"Resolve was called with unexpected module request, {moduleRequest.Specifier} relative to {referencingModuleLocation}");
- return new ResolvedSpecifier(moduleRequest, target.ToString(), target, SpecifierType.Bare);
- }
- public Module LoadModule(Engine engine, ResolvedSpecifier resolved)
- {
- Assert.NotNull(resolved.Uri);
- var source = resolved.Uri.ToString();
- Assert.True(_modules.TryGetValue(source, out var script), $"Resolved module does not exist: {source}");
- return ModuleFactory.BuildSourceTextModule(engine, Engine.PrepareModule(script, source));
- }
- }
- private static string GetBasePath()
- {
- var assemblyDirectory = new DirectoryInfo(AppDomain.CurrentDomain.RelativeSearchPath ?? AppDomain.CurrentDomain.BaseDirectory);
- var current = assemblyDirectory;
- var binDirectory = $"{Path.DirectorySeparatorChar}bin{Path.DirectorySeparatorChar}";
- while (current is not null)
- {
- if (current.FullName.Contains(binDirectory) || current.Name == "bin")
- {
- current = current.Parent;
- continue;
- }
- var testDirectory = current.GetDirectories("Jint.Tests").FirstOrDefault();
- if (testDirectory == null)
- {
- current = current.Parent;
- continue;
- }
- // found it
- current = testDirectory;
- break;
- }
- if (current is null)
- {
- throw new NullReferenceException($"Could not find tests base path, assemblyPath: {assemblyDirectory}");
- }
- return Path.Combine(current.FullName, "Runtime", "Scripts");
- }
- [Fact]
- public void ModuleBuilderWithCustomModuleLoaderLoadsModulesProperly()
- {
- var engine = new Engine(o => o.EnableModules(new LocationResolveOnlyModuleLoader((_, moduleRequest) =>
- {
- var result = moduleRequest.Specifier;
- if (moduleRequest.Specifier == "../library1/builder_module.js")
- {
- result = "library1/builder_module.js";
- }
- return result;
- })));
- var logStatements = new List<string>();
- engine.SetValue("log", logStatements.Add);
- engine.Modules.Add("library1/builder_module.js",
- builder => builder.AddSource("export const value = 'builder_module_const'; log('builder_module')"));
- engine.Modules.Add("library2/entry_point_module.js",
- builder => builder.AddSource("import * as m from '../library1/builder_module.js'; log('entry_point_module')"));
- engine.Modules.Import("library2/entry_point_module.js");
- Assert.Collection(
- logStatements,
- s => Assert.Equal("builder_module", s),
- s => Assert.Equal("entry_point_module", s));
- }
- [Fact]
- public void ModuleBuilderPassesReferencingModuleLocationToModuleLoader()
- {
- var engine = new Engine(o => o.EnableModules(new LocationResolveOnlyModuleLoader((referencingModuleLocation, moduleRequest) =>
- {
- var result = moduleRequest.Specifier;
- if (moduleRequest.Specifier == "../library1/builder_module.js")
- {
- Assert.Equal("library2/entry_point_module.js", referencingModuleLocation);
- result = "library1/builder_module.js";
- }
- return result;
- })));
- var logStatements = new List<string>();
- engine.SetValue("log", logStatements.Add);
- engine.Modules.Add("library1/builder_module.js",
- builder => builder.AddSource("export const value = 'builder_module_const'; log('builder_module')"));
- engine.Modules.Add("library2/entry_point_module.js",
- builder => builder.AddSource("import * as m from '../library1/builder_module.js'; log('entry_point_module')"));
- engine.Modules.Import("library2/entry_point_module.js");
- Assert.Collection(
- logStatements,
- s => Assert.Equal("builder_module", s),
- s => Assert.Equal("entry_point_module", s));
- }
- /// <summary>
- /// Custom <see cref="ModuleLoader"/> implementation which is only responsible to
- /// resolve the correct module location (see <see cref="Resolve"/>). Modules
- /// must be registered using <see cref="Jint.Engine.ModuleOperations"/> (e.g.
- /// by using <see cref="Engine.ModuleOperations.Add(string,string)"/>)
- /// </summary>
- private sealed class LocationResolveOnlyModuleLoader : ModuleLoader
- {
- public delegate string ResolveHandler(string referencingModuleLocation, ModuleRequest moduleRequest);
- private readonly ResolveHandler _resolveHandler;
- public LocationResolveOnlyModuleLoader(ResolveHandler resolveHandler)
- {
- _resolveHandler = resolveHandler ?? throw new ArgumentNullException(nameof(resolveHandler));
- }
- public override ResolvedSpecifier Resolve(string referencingModuleLocation, ModuleRequest moduleRequest)
- {
- return new ResolvedSpecifier(
- moduleRequest,
- Key: _resolveHandler(referencingModuleLocation, moduleRequest),
- Uri: null,
- SpecifierType.RelativeOrAbsolute
- );
- }
- protected override string LoadModuleContents(Engine engine, ResolvedSpecifier resolved)
- => throw new InvalidOperationException();
- }
- [Fact]
- public void EngineShouldTransmitSourceModuleForModuleLoader()
- {
- var engine = new Engine(o => o.EnableModules(new ModuleLoaderForEngineShouldTransmitSourceModuleForModuleLoaderTest()));
- var logs = new List<string>();
- engine.SetValue("log", logs.Add);
- engine.Modules.Import($"code/lib/module.js");
- Assert.Collection(logs,
- s => Assert.Equal("code/execute.js", s),
- s => Assert.Equal("code/lib/module.js", s));
- }
- public class ModuleLoaderForEngineShouldTransmitSourceModuleForModuleLoaderTest : ModuleLoader
- {
- public override ResolvedSpecifier Resolve(string referencingModuleLocation, ModuleRequest moduleRequest)
- {
- var moduleSpec = moduleRequest.Specifier;
- // to resolve this statement requires information about source module
- if (moduleSpec == "../execute.js")
- {
- Assert.True(!string.IsNullOrEmpty(referencingModuleLocation), "module loader cannot resolve referensing module - has no referencing module location");
- moduleSpec = $"code/execute.js";
- }
- return new ResolvedSpecifier(
- moduleRequest,
- moduleSpec,
- Uri: null,
- SpecifierType.RelativeOrAbsolute
- );
- }
- protected override string LoadModuleContents(Engine engine, ResolvedSpecifier resolved)
- {
- if (resolved.Key == $"code/lib/module.js")
- return $"import * as m from '../execute.js'; log('code/lib/module.js')";
- if (resolved.Key == $"code/execute.js")
- {
- return $"log('code/execute.js')";
- }
- throw new NotImplementedException(); // no need in this test
- }
- }
- }
|