ModuleTests.cs 13 KB

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