ModuleTests.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
  1. using System.IO;
  2. using System.Reflection;
  3. using System;
  4. using System.Collections.Generic;
  5. using System.Linq;
  6. using Jint.Native;
  7. using Jint.Runtime;
  8. using Xunit;
  9. namespace Jint.Tests.Runtime;
  10. public class ModuleTests
  11. {
  12. private readonly Engine _engine;
  13. public ModuleTests()
  14. {
  15. _engine = new Engine();
  16. }
  17. [Fact]
  18. public void ShouldExportNamed()
  19. {
  20. _engine.AddModule("my-module", @"export const value = 'exported value';");
  21. var ns = _engine.ImportModule("my-module");
  22. Assert.Equal("exported value", ns.Get("value").AsString());
  23. }
  24. [Fact]
  25. public void ShouldExportNamedListRenamed()
  26. {
  27. _engine.AddModule("my-module", @"const value1 = 1; const value2 = 2; export { value1 as renamed1, value2 as renamed2 }");
  28. var ns = _engine.ImportModule("my-module");
  29. Assert.Equal(1, ns.Get("renamed1").AsInteger());
  30. Assert.Equal(2, ns.Get("renamed2").AsInteger());
  31. }
  32. [Fact]
  33. public void ShouldExportDefault()
  34. {
  35. _engine.AddModule("my-module", @"export default 'exported value';");
  36. var ns = _engine.ImportModule("my-module");
  37. Assert.Equal("exported value", ns.Get("default").AsString());
  38. }
  39. [Fact]
  40. public void ShouldExportAll()
  41. {
  42. _engine.AddModule("module1", @"export const value = 'exported value';");
  43. _engine.AddModule("module2", @"export * from 'module1';");
  44. var ns = _engine.ImportModule("module2");
  45. Assert.Equal("exported value", ns.Get("value").AsString());
  46. }
  47. [Fact]
  48. public void ShouldImportNamed()
  49. {
  50. _engine.AddModule("imported-module", @"export const value = 'exported value';");
  51. _engine.AddModule("my-module", @"import { value } from 'imported-module'; export const exported = value;");
  52. var ns = _engine.ImportModule("my-module");
  53. Assert.Equal("exported value", ns.Get("exported").AsString());
  54. }
  55. [Fact]
  56. public void ShouldImportRenamed()
  57. {
  58. _engine.AddModule("imported-module", @"export const value = 'exported value';");
  59. _engine.AddModule("my-module", @"import { value as renamed } from 'imported-module'; export const exported = renamed;");
  60. var ns = _engine.ImportModule("my-module");
  61. Assert.Equal("exported value", ns.Get("exported").AsString());
  62. }
  63. [Fact]
  64. public void ShouldImportDefault()
  65. {
  66. _engine.AddModule("imported-module", @"export default 'exported value';");
  67. _engine.AddModule("my-module", @"import imported from 'imported-module'; export const exported = imported;");
  68. var ns = _engine.ImportModule("my-module");
  69. Assert.Equal("exported value", ns.Get("exported").AsString());
  70. }
  71. [Fact]
  72. public void ShouldImportAll()
  73. {
  74. _engine.AddModule("imported-module", @"export const value = 'exported value';");
  75. _engine.AddModule("my-module", @"import * as imported from 'imported-module'; export const exported = imported.value;");
  76. var ns = _engine.ImportModule("my-module");
  77. Assert.Equal("exported value", ns.Get("exported").AsString());
  78. }
  79. [Fact]
  80. public void ShouldImportDynamically()
  81. {
  82. var received = false;
  83. _engine.AddModule("imported-module", builder => builder.ExportFunction("signal", () => received = true));
  84. _engine.AddModule("my-module", @"import('imported-module').then(ns => { ns.signal(); });");
  85. _engine.ImportModule("my-module");
  86. _engine.RunAvailableContinuations();
  87. Assert.True(received);
  88. }
  89. [Fact]
  90. public void ShouldPropagateParseError()
  91. {
  92. _engine.AddModule("imported", @"export const invalid;");
  93. _engine.AddModule("my-module", @"import { invalid } from 'imported';");
  94. var exc = Assert.Throws<JavaScriptException>(() => _engine.ImportModule("my-module"));
  95. Assert.Equal("Error while loading module: error in module 'imported': Line 1: Missing initializer in const declaration", exc.Message);
  96. }
  97. [Fact]
  98. public void ShouldPropagateLinkError()
  99. {
  100. _engine.AddModule("imported", @"export invalid;");
  101. _engine.AddModule("my-module", @"import { value } from 'imported';");
  102. var exc = Assert.Throws<JavaScriptException>(() => _engine.ImportModule("my-module"));
  103. Assert.Equal("Error while loading module: error in module 'imported': Line 1: Unexpected identifier", exc.Message);
  104. Assert.Equal("my-module", exc.Location.Source);
  105. }
  106. [Fact]
  107. public void ShouldPropagateExecuteError()
  108. {
  109. _engine.AddModule("my-module", @"throw new Error('imported successfully');");
  110. var exc = Assert.Throws<JavaScriptException>(() => _engine.ImportModule("my-module"));
  111. Assert.Equal("imported successfully", exc.Message);
  112. Assert.Equal("my-module", exc.Location.Source);
  113. }
  114. [Fact]
  115. public void ShouldPropagateThrowStatementThroughJavaScriptImport()
  116. {
  117. _engine.AddModule("imported-module", @"throw new Error('imported successfully');");
  118. _engine.AddModule("my-module", @"import 'imported-module';");
  119. var exc = Assert.Throws<JavaScriptException>(() => _engine.ImportModule("my-module"));
  120. Assert.Equal("imported successfully", exc.Message);
  121. }
  122. [Fact]
  123. public void ShouldAddModuleFromJsValue()
  124. {
  125. _engine.AddModule("my-module", builder => builder.ExportValue("value", JsString.Create("hello world")));
  126. var ns = _engine.ImportModule("my-module");
  127. Assert.Equal("hello world", ns.Get("value").AsString());
  128. }
  129. [Fact]
  130. public void ShouldAddModuleFromClrInstance()
  131. {
  132. _engine.AddModule("imported-module", builder => builder.ExportObject("value", new ImportedClass { Value = "instance value" }));
  133. _engine.AddModule("my-module", @"import { value } from 'imported-module'; export const exported = value.value;");
  134. var ns = _engine.ImportModule("my-module");
  135. Assert.Equal("instance value", ns.Get("exported").AsString());
  136. }
  137. [Fact]
  138. public void ShouldAllowInvokeUserDefinedClass()
  139. {
  140. _engine.AddModule("user", "export class UserDefined { constructor(v) { this._v = v; } hello(c) { return `hello ${this._v}${c}`; } }");
  141. var ctor = _engine.ImportModule("user").Get("UserDefined");
  142. var instance = _engine.Construct(ctor, JsString.Create("world"));
  143. var result = instance.GetMethod("hello").Call(instance, JsString.Create("!"));
  144. Assert.Equal("hello world!", result);
  145. }
  146. [Fact]
  147. public void ShouldAddModuleFromClrType()
  148. {
  149. _engine.AddModule("imported-module", builder => builder.ExportType<ImportedClass>());
  150. _engine.AddModule("my-module", @"import { ImportedClass } from 'imported-module'; export const exported = new ImportedClass().value;");
  151. var ns = _engine.ImportModule("my-module");
  152. Assert.Equal("hello world", ns.Get("exported").AsString());
  153. }
  154. [Fact]
  155. public void ShouldAddModuleFromClrFunction()
  156. {
  157. var received = new List<string>();
  158. _engine.AddModule("imported-module", builder => builder
  159. .ExportFunction("act_noargs", () => received.Add("act_noargs"))
  160. .ExportFunction("act_args", args => received.Add($"act_args:{args[0].AsString()}"))
  161. .ExportFunction("fn_noargs", () =>
  162. {
  163. received.Add("fn_noargs");
  164. return "ret";
  165. })
  166. .ExportFunction("fn_args", args =>
  167. {
  168. received.Add($"fn_args:{args[0].AsString()}");
  169. return "ret";
  170. })
  171. );
  172. _engine.AddModule("my-module", @"
  173. import * as fns from 'imported-module';
  174. export const result = [fns.act_noargs(), fns.act_args('ok'), fns.fn_noargs(), fns.fn_args('ok')];");
  175. var ns = _engine.ImportModule("my-module");
  176. Assert.Equal(new[] { "act_noargs", "act_args:ok", "fn_noargs", "fn_args:ok" }, received.ToArray());
  177. Assert.Equal(new[] { "undefined", "undefined", "ret", "ret" }, ns.Get("result").AsArray().Select(x => x.ToString()).ToArray());
  178. }
  179. private class ImportedClass
  180. {
  181. public string Value { get; set; } = "hello world";
  182. }
  183. [Fact]
  184. public void ShouldAllowExportMultipleImports()
  185. {
  186. _engine.AddModule("@mine/import1", builder => builder.ExportValue("value1", JsNumber.Create(1)));
  187. _engine.AddModule("@mine/import2", builder => builder.ExportValue("value2", JsNumber.Create(2)));
  188. _engine.AddModule("@mine", "export * from '@mine/import1'; export * from '@mine/import2'");
  189. _engine.AddModule("app", @"import { value1, value2 } from '@mine'; export const result = `${value1} ${value2}`");
  190. var ns = _engine.ImportModule("app");
  191. Assert.Equal("1 2", ns.Get("result").AsString());
  192. }
  193. [Fact]
  194. public void ShouldAllowNamedStarExport()
  195. {
  196. _engine.AddModule("imported-module", builder => builder.ExportValue("value1", 5));
  197. _engine.AddModule("my-module", "export * as ns from 'imported-module';");
  198. var ns = _engine.ImportModule("my-module");
  199. Assert.Equal(5, ns.Get("ns").Get("value1").AsNumber());
  200. }
  201. [Fact]
  202. public void ShouldAllowChaining()
  203. {
  204. _engine.AddModule("dependent-module", "export const dependency = 1;");
  205. _engine.AddModule("my-module", builder => builder
  206. .AddSource("import { dependency } from 'dependent-module';")
  207. .AddSource("export const output = dependency + 1;")
  208. .ExportValue("num", JsNumber.Create(-1))
  209. );
  210. var ns = _engine.ImportModule("my-module");
  211. Assert.Equal(2, ns.Get("output").AsInteger());
  212. Assert.Equal(-1, ns.Get("num").AsInteger());
  213. }
  214. [Fact]
  215. public void ShouldImportOnlyOnce()
  216. {
  217. var called = 0;
  218. _engine.AddModule("imported-module", builder => builder.ExportFunction("count", args => called++));
  219. _engine.AddModule("my-module", @"import { count } from 'imported-module'; count();");
  220. _engine.ImportModule("my-module");
  221. _engine.ImportModule("my-module");
  222. Assert.Equal(1, called);
  223. }
  224. [Fact]
  225. public void ShouldAllowSelfImport()
  226. {
  227. _engine.AddModule("my-globals", @"export const globals = { counter: 0 };");
  228. _engine.AddModule("my-module", @"
  229. import { globals } from 'my-globals';
  230. import {} from 'my-module';
  231. globals.counter++;
  232. export const count = globals.counter;
  233. ");
  234. var ns= _engine.ImportModule("my-module");
  235. Assert.Equal(1, ns.Get("count").AsInteger());
  236. }
  237. [Fact]
  238. public void ShouldAllowCyclicImport()
  239. {
  240. // https://tc39.es/ecma262/#sec-example-cyclic-module-record-graphs
  241. _engine.AddModule("B", @"import { a } from 'A'; export const b = 'b';");
  242. _engine.AddModule("A", @"import { b } from 'B'; export const a = 'a';");
  243. var nsA = _engine.ImportModule("A");
  244. var nsB = _engine.ImportModule("B");
  245. Assert.Equal("a", nsA.Get("a").AsString());
  246. Assert.Equal("b", nsB.Get("b").AsString());
  247. }
  248. [Fact]
  249. public void ShouldSupportConstraints()
  250. {
  251. var engine = new Engine(opts => opts.TimeoutInterval(TimeSpan.FromTicks(1)));
  252. engine.AddModule("my-module", @"for(var i = 0; i < 100000; i++) { } export const result = 'ok';");
  253. Assert.Throws<TimeoutException>(() => engine.ImportModule("my-module"));
  254. }
  255. [Fact]
  256. public void CanLoadModuleImportsFromFiles()
  257. {
  258. var engine = new Engine(options => options.EnableModules(GetBasePath()));
  259. engine.AddModule("my-module", "import { User } from './modules/user.js'; export const user = new User('John', 'Doe');");
  260. var ns = engine.ImportModule("my-module");
  261. Assert.Equal("John Doe", ns["user"].Get("name").AsString());
  262. }
  263. [Fact]
  264. public void CanImportFromFile()
  265. {
  266. var engine = new Engine(options => options.EnableModules(GetBasePath()));
  267. var ns = engine.ImportModule("./modules/format-name.js");
  268. var result = engine.Invoke(ns.Get("formatName"), "John", "Doe").AsString();
  269. Assert.Equal("John Doe", result);
  270. }
  271. internal static string GetBasePath()
  272. {
  273. var assemblyDirectory = new DirectoryInfo(AppDomain.CurrentDomain.RelativeSearchPath ?? AppDomain.CurrentDomain.BaseDirectory);
  274. var current = assemblyDirectory;
  275. while (current is not null && current.Name != "Jint.Tests")
  276. {
  277. current = current.Parent;
  278. }
  279. if (current is null)
  280. {
  281. throw new NullReferenceException($"Could not find tests base path, assemblyPath: {assemblyDirectory}");
  282. }
  283. return Path.Combine(current.FullName, "Runtime", "Scripts");
  284. }
  285. }