2
0

ModuleTests.cs 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782
  1. using Jint.Native;
  2. using Jint.Runtime;
  3. using Jint.Runtime.Modules;
  4. using Module = Jint.Runtime.Modules.Module;
  5. namespace Jint.Tests.Runtime;
  6. public class ModuleTests
  7. {
  8. private readonly Engine _engine;
  9. public ModuleTests()
  10. {
  11. _engine = new Engine();
  12. }
  13. [Fact]
  14. public void ShouldExportNamed()
  15. {
  16. _engine.Modules.Add("my-module", "export const value = 'exported value';");
  17. var ns = _engine.Modules.Import("my-module");
  18. Assert.Equal("exported value", ns.Get("value").AsString());
  19. }
  20. [Fact]
  21. public void ShouldExportNamedListRenamed()
  22. {
  23. _engine.Modules.Add("my-module", "const value1 = 1; const value2 = 2; export { value1 as renamed1, value2 as renamed2 }");
  24. var ns = _engine.Modules.Import("my-module");
  25. Assert.Equal(1, ns.Get("renamed1").AsInteger());
  26. Assert.Equal(2, ns.Get("renamed2").AsInteger());
  27. }
  28. [Fact]
  29. public void ShouldExportDefault()
  30. {
  31. _engine.Modules.Add("my-module", "export default 'exported value';");
  32. var ns = _engine.Modules.Import("my-module");
  33. Assert.Equal("exported value", ns.Get("default").AsString());
  34. }
  35. [Fact]
  36. public void ShouldExportDefaultFunctionWithoutName()
  37. {
  38. _engine.Modules.Add("module1", "export default function main() { return 1; }");
  39. _engine.Modules.Add("module2", "export default function () { return 1; }");
  40. var ns = _engine.Modules.Import("module1");
  41. var func = ns.Get("default");
  42. Assert.Equal(1, func.Call());
  43. ns = _engine.Modules.Import("module2");
  44. func = ns.Get("default");
  45. Assert.Equal(1, func.Call());
  46. }
  47. [Fact]
  48. public void ShouldExportAll()
  49. {
  50. _engine.Modules.Add("module1", "export const value = 'exported value';");
  51. _engine.Modules.Add("module2", "export * from 'module1';");
  52. var ns = _engine.Modules.Import("module2");
  53. Assert.Equal("exported value", ns.Get("value").AsString());
  54. }
  55. [Fact]
  56. public void ShouldImportNamed()
  57. {
  58. _engine.Modules.Add("imported-module", "export const value = 'exported value';");
  59. _engine.Modules.Add("my-module", "import { value } from 'imported-module'; export const exported = value;");
  60. var ns = _engine.Modules.Import("my-module");
  61. Assert.Equal("exported value", ns.Get("exported").AsString());
  62. }
  63. [Fact]
  64. public void ShouldImportRenamed()
  65. {
  66. _engine.Modules.Add("imported-module", "export const value = 'exported value';");
  67. _engine.Modules.Add("my-module", "import { value as renamed } from 'imported-module'; export const exported = renamed;");
  68. var ns = _engine.Modules.Import("my-module");
  69. Assert.Equal("exported value", ns.Get("exported").AsString());
  70. }
  71. [Fact]
  72. public void ShouldImportDefault()
  73. {
  74. _engine.Modules.Add("imported-module", "export default 'exported value';");
  75. _engine.Modules.Add("my-module", "import imported from 'imported-module'; export const exported = imported;");
  76. var ns = _engine.Modules.Import("my-module");
  77. Assert.Equal("exported value", ns.Get("exported").AsString());
  78. }
  79. [Fact]
  80. public void ShouldImportAll()
  81. {
  82. _engine.Modules.Add("imported-module", "export const value = 'exported value';");
  83. _engine.Modules.Add("my-module", "import * as imported from 'imported-module'; export const exported = imported.value;");
  84. var ns = _engine.Modules.Import("my-module");
  85. Assert.Equal("exported value", ns.Get("exported").AsString());
  86. }
  87. [Fact]
  88. public void ShouldImportDynamically()
  89. {
  90. var received = false;
  91. _engine.Modules.Add("imported-module", builder => builder.ExportFunction("signal", () => received = true));
  92. _engine.Modules.Add("my-module", "import('imported-module').then(ns => { ns.signal(); });");
  93. _engine.Modules.Import("my-module");
  94. Assert.True(received);
  95. }
  96. [Fact]
  97. public void ShouldPropagateParseError()
  98. {
  99. _engine.Modules.Add("imported", "export const invalid;");
  100. _engine.Modules.Add("my-module", "import { invalid } from 'imported';");
  101. var exc = Assert.Throws<JavaScriptException>(() => _engine.Modules.Import("my-module"));
  102. Assert.Equal("Error while loading module: error in module 'imported': Missing initializer in const declaration (imported:1:21)", exc.Message);
  103. Assert.Equal("imported", exc.Location.SourceFile);
  104. }
  105. [Fact]
  106. public void ShouldPropagateLinkError()
  107. {
  108. _engine.Modules.Add("imported", "export invalid;");
  109. _engine.Modules.Add("my-module", "import { value } from 'imported';");
  110. var exc = Assert.Throws<JavaScriptException>(() => _engine.Modules.Import("my-module"));
  111. Assert.Equal("Error while loading module: error in module 'imported': Unexpected identifier 'invalid' (imported:1:8)", exc.Message);
  112. Assert.Equal("imported", exc.Location.SourceFile);
  113. }
  114. [Fact]
  115. public void ShouldPropagateExecuteError()
  116. {
  117. _engine.Modules.Add("my-module", "throw new Error('imported successfully');");
  118. var exc = Assert.Throws<JavaScriptException>(() => _engine.Modules.Import("my-module"));
  119. Assert.Equal("imported successfully", exc.Message);
  120. Assert.Equal("my-module", exc.Location.SourceFile);
  121. }
  122. [Fact]
  123. public void ShouldPropagateThrowStatementThroughJavaScriptImport()
  124. {
  125. _engine.Modules.Add("imported-module", "throw new Error('imported successfully');");
  126. _engine.Modules.Add("my-module", "import 'imported-module';");
  127. var exc = Assert.Throws<JavaScriptException>(() => _engine.Modules.Import("my-module"));
  128. Assert.Equal("imported successfully", exc.Message);
  129. }
  130. [Fact]
  131. public void ShouldAddModuleFromJsValue()
  132. {
  133. _engine.Modules.Add("my-module", builder => builder.ExportValue("value", JsString.Create("hello world")));
  134. var ns = _engine.Modules.Import("my-module");
  135. Assert.Equal("hello world", ns.Get("value").AsString());
  136. }
  137. [Fact]
  138. public void ShouldAddModuleFromClrInstance()
  139. {
  140. _engine.Modules.Add("imported-module", builder => builder.ExportObject("value", new ImportedClass
  141. {
  142. Value = "instance value"
  143. }));
  144. _engine.Modules.Add("my-module", "import { value } from 'imported-module'; export const exported = value.value;");
  145. var ns = _engine.Modules.Import("my-module");
  146. Assert.Equal("instance value", ns.Get("exported").AsString());
  147. }
  148. [Fact]
  149. public void ShouldAllowInvokeUserDefinedClass()
  150. {
  151. _engine.Modules.Add("user", "export class UserDefined { constructor(v) { this._v = v; } hello(c) { return `hello ${this._v}${c}`; } }");
  152. var ctor = _engine.Modules.Import("user").Get("UserDefined");
  153. var instance = _engine.Construct(ctor, JsString.Create("world"));
  154. var result = instance.GetMethod("hello").Call(instance, JsString.Create("!"));
  155. Assert.Equal("hello world!", result);
  156. }
  157. [Fact]
  158. public void ShouldAddModuleFromClrType()
  159. {
  160. _engine.Modules.Add("imported-module", builder => builder.ExportType<ImportedClass>());
  161. _engine.Modules.Add("my-module", "import { ImportedClass } from 'imported-module'; export const exported = new ImportedClass().value;");
  162. var ns = _engine.Modules.Import("my-module");
  163. Assert.Equal("hello world", ns.Get("exported").AsString());
  164. }
  165. [Fact]
  166. public void ShouldAddModuleFromClrFunction()
  167. {
  168. var received = new List<string>();
  169. _engine.Modules.Add("imported-module", builder => builder
  170. .ExportFunction("act_noargs", () => received.Add("act_noargs"))
  171. .ExportFunction("act_args", args => received.Add($"act_args:{args[0].AsString()}"))
  172. .ExportFunction("fn_noargs", () =>
  173. {
  174. received.Add("fn_noargs");
  175. return "ret";
  176. })
  177. .ExportFunction("fn_args", args =>
  178. {
  179. received.Add($"fn_args:{args[0].AsString()}");
  180. return "ret";
  181. })
  182. );
  183. _engine.Modules.Add("my-module", @"
  184. import * as fns from 'imported-module';
  185. export const result = [fns.act_noargs(), fns.act_args('ok'), fns.fn_noargs(), fns.fn_args('ok')];");
  186. var ns = _engine.Modules.Import("my-module");
  187. Assert.Equal(new[]
  188. {
  189. "act_noargs",
  190. "act_args:ok",
  191. "fn_noargs",
  192. "fn_args:ok"
  193. }, received.ToArray());
  194. Assert.Equal(new[]
  195. {
  196. "undefined",
  197. "undefined",
  198. "ret",
  199. "ret"
  200. }, ns.Get("result").AsArray().Select(x => x.ToString()).ToArray());
  201. }
  202. private class ImportedClass
  203. {
  204. public string Value { get; set; } = "hello world";
  205. }
  206. [Fact]
  207. public void ShouldAllowExportMultipleImports()
  208. {
  209. _engine.Modules.Add("@mine/import1", builder => builder.ExportValue("value1", JsNumber.Create(1)));
  210. _engine.Modules.Add("@mine/import2", builder => builder.ExportValue("value2", JsNumber.Create(2)));
  211. _engine.Modules.Add("@mine", "export * from '@mine/import1'; export * from '@mine/import2'");
  212. _engine.Modules.Add("app", "import { value1, value2 } from '@mine'; export const result = `${value1} ${value2}`");
  213. var ns = _engine.Modules.Import("app");
  214. Assert.Equal("1 2", ns.Get("result").AsString());
  215. }
  216. [Fact]
  217. public void ShouldAllowNamedStarExport()
  218. {
  219. _engine.Modules.Add("imported-module", builder => builder.ExportValue("value1", 5));
  220. _engine.Modules.Add("my-module", "export * as ns from 'imported-module';");
  221. var ns = _engine.Modules.Import("my-module");
  222. Assert.Equal(5, ns.Get("ns").Get("value1").AsNumber());
  223. }
  224. [Fact]
  225. public void ShouldAllowChaining()
  226. {
  227. _engine.Modules.Add("dependent-module", "export const dependency = 1;");
  228. _engine.Modules.Add("my-module", builder => builder
  229. .AddSource("import { dependency } from 'dependent-module';")
  230. .AddSource("export const output = dependency + 1;")
  231. .ExportValue("num", JsNumber.Create(-1))
  232. );
  233. var ns = _engine.Modules.Import("my-module");
  234. Assert.Equal(2, ns.Get("output").AsInteger());
  235. Assert.Equal(-1, ns.Get("num").AsInteger());
  236. }
  237. [Fact]
  238. public void ShouldImportOnlyOnce()
  239. {
  240. var called = 0;
  241. _engine.Modules.Add("imported-module", builder => builder.ExportFunction("count", args => called++));
  242. _engine.Modules.Add("my-module", "import { count } from 'imported-module'; count();");
  243. _engine.Modules.Import("my-module");
  244. _engine.Modules.Import("my-module");
  245. Assert.Equal(1, called);
  246. }
  247. [Fact]
  248. public void ShouldAllowSelfImport()
  249. {
  250. _engine.Modules.Add("my-globals", "export const globals = { counter: 0 };");
  251. _engine.Modules.Add("my-module", @"
  252. import { globals } from 'my-globals';
  253. import {} from 'my-module';
  254. globals.counter++;
  255. export const count = globals.counter;
  256. ");
  257. var ns = _engine.Modules.Import("my-module");
  258. Assert.Equal(1, ns.Get("count").AsInteger());
  259. }
  260. [Fact]
  261. public void ShouldAllowCyclicImport()
  262. {
  263. // https://tc39.es/ecma262/#sec-example-cyclic-module-record-graphs
  264. _engine.Modules.Add("B", "import { a } from 'A'; export const b = 'b';");
  265. _engine.Modules.Add("A", "import { b } from 'B'; export const a = 'a';");
  266. var nsA = _engine.Modules.Import("A");
  267. var nsB = _engine.Modules.Import("B");
  268. Assert.Equal("a", nsA.Get("a").AsString());
  269. Assert.Equal("b", nsB.Get("b").AsString());
  270. }
  271. [Fact]
  272. public void ShouldSupportConstraints()
  273. {
  274. var engine = new Engine(opts => opts.TimeoutInterval(TimeSpan.FromTicks(1)));
  275. engine.Modules.Add("sleep", builder => builder.ExportFunction("sleep", () => Thread.Sleep(100)));
  276. engine.Modules.Add("my-module", "import { sleep } from 'sleep'; for(var i = 0; i < 100; i++) { sleep(); } export const result = 'ok';");
  277. Assert.Throws<TimeoutException>(() => engine.Modules.Import("my-module"));
  278. }
  279. [Fact]
  280. public void CanLoadModuleImportsFromFiles()
  281. {
  282. var engine = new Engine(options => options.EnableModules(GetBasePath()));
  283. engine.Modules.Add("my-module", "import { User } from './modules/user.js'; export const user = new User('John', 'Doe');");
  284. var ns = engine.Modules.Import("my-module");
  285. Assert.Equal("John Doe", ns["user"].Get("name").AsString());
  286. }
  287. [Fact]
  288. public void CanImportFromFile()
  289. {
  290. var engine = new Engine(options => options.EnableModules(GetBasePath()));
  291. var ns = engine.Modules.Import("./modules/format-name.js");
  292. var result = engine.Invoke(ns.Get("formatName"), "John", "Doe").AsString();
  293. Assert.Equal("John Doe", result);
  294. }
  295. [Fact]
  296. public void CanImportFromFileWithSpacesInPath()
  297. {
  298. var engine = new Engine(options => options.EnableModules(GetBasePath()));
  299. var ns = engine.Modules.Import("./dir with spaces/format name.js");
  300. var result = engine.Invoke(ns.Get("formatName"), "John", "Doe").AsString();
  301. Assert.Equal("John Doe", result);
  302. }
  303. [Fact]
  304. public void CanReuseModule()
  305. {
  306. const string Code = "export function formatName(firstName, lastName) {\r\n return `${firstName} ${lastName}`;\r\n}";
  307. var module = Engine.PrepareModule(Code);
  308. for (var i = 0; i < 5; i++)
  309. {
  310. var engine = new Engine();
  311. engine.Modules.Add("__main__", x => x.AddModule(module));
  312. var ns = engine.Modules.Import("__main__");
  313. var result = engine.Invoke(ns.Get("formatName"), "John" + i, "Doe").AsString();
  314. Assert.Equal($"John{i} Doe", result);
  315. }
  316. }
  317. [Fact]
  318. public void EngineExecutePassesSourceForModuleResolving()
  319. {
  320. var moduleLoader = new EnforceRelativeModuleLoader(new Dictionary<string, string>()
  321. {
  322. ["file:///folder/my-module.js"] = "export const value = 'myModuleConst'"
  323. });
  324. var engine = new Engine(options => options.EnableModules(moduleLoader));
  325. var code = @"
  326. (async () => {
  327. const { value } = await import('./my-module.js');
  328. log(value);
  329. })();
  330. ";
  331. List<string> logStatements = new List<string>();
  332. engine.SetValue("log", logStatements.Add);
  333. engine.Execute(code, source: "file:///folder/main.js");
  334. engine.Advanced.ProcessTasks();
  335. Assert.Collection(
  336. logStatements,
  337. s => Assert.Equal("myModuleConst", s));
  338. }
  339. [Fact]
  340. public void EngineExecuteUsesScriptSourceForSource()
  341. {
  342. var moduleLoader = new EnforceRelativeModuleLoader(new Dictionary<string, string>()
  343. {
  344. ["file:///folder/my-module.js"] = "export const value = 'myModuleConst'"
  345. });
  346. var engine = new Engine(options => options.EnableModules(moduleLoader));
  347. var code = @"
  348. (async () => {
  349. const { value } = await import('./my-module.js');
  350. log(value);
  351. })();
  352. ";
  353. List<string> logStatements = new List<string>();
  354. engine.SetValue("log", logStatements.Add);
  355. var script = Engine.PrepareScript(code, source: "file:///folder/main.js");
  356. engine.Execute(script);
  357. engine.Advanced.ProcessTasks();
  358. Assert.Collection(
  359. logStatements,
  360. s => Assert.Equal("myModuleConst", s));
  361. }
  362. [Fact]
  363. public void EngineEvaluatePassesSourceForModuleResolving()
  364. {
  365. var moduleLoader = new EnforceRelativeModuleLoader(new Dictionary<string, string>()
  366. {
  367. ["file:///folder/my-module.js"] = "export const value = 'myModuleConst'"
  368. });
  369. var engine = new Engine(options => options.EnableModules(moduleLoader));
  370. var code = @"
  371. (async () => {
  372. const { value } = await import('./my-module.js');
  373. log(value);
  374. })();
  375. ";
  376. List<string> logStatements = new List<string>();
  377. engine.SetValue("log", logStatements.Add);
  378. engine.Evaluate(code, source: "file:///folder/main.js");
  379. engine.Advanced.ProcessTasks();
  380. Assert.Collection(
  381. logStatements,
  382. s => Assert.Equal("myModuleConst", s));
  383. }
  384. [Fact]
  385. public void EngineEvaluateUsesScriptSourceForSource()
  386. {
  387. var moduleLoader = new EnforceRelativeModuleLoader(new Dictionary<string, string>()
  388. {
  389. ["file:///folder/my-module.js"] = "export const value = 'myModuleConst'"
  390. });
  391. var engine = new Engine(options => options.EnableModules(moduleLoader));
  392. var code = @"
  393. (async () => {
  394. const { value } = await import('./my-module.js');
  395. log(value);
  396. })();
  397. ";
  398. List<string> logStatements = new List<string>();
  399. engine.SetValue("log", logStatements.Add);
  400. var script = Engine.PrepareScript(code, source: "file:///folder/main.js");
  401. engine.Evaluate(script);
  402. engine.Advanced.ProcessTasks();
  403. Assert.Collection(
  404. logStatements,
  405. s => Assert.Equal("myModuleConst", s));
  406. }
  407. private sealed class EnforceRelativeModuleLoader : IModuleLoader
  408. {
  409. private readonly IReadOnlyDictionary<string, string> _modules;
  410. public EnforceRelativeModuleLoader(IReadOnlyDictionary<string, string> modules)
  411. {
  412. _modules = modules;
  413. }
  414. public ResolvedSpecifier Resolve(string referencingModuleLocation, ModuleRequest moduleRequest)
  415. {
  416. Assert.False(string.IsNullOrEmpty(referencingModuleLocation), "Referencing module location is null or empty");
  417. var target = new Uri(new Uri(referencingModuleLocation, UriKind.Absolute), moduleRequest.Specifier);
  418. Assert.True(_modules.ContainsKey(target.ToString()), $"Resolve was called with unexpected module request, {moduleRequest.Specifier} relative to {referencingModuleLocation}");
  419. return new ResolvedSpecifier(moduleRequest, target.ToString(), target, SpecifierType.Bare);
  420. }
  421. public Module LoadModule(Engine engine, ResolvedSpecifier resolved)
  422. {
  423. Assert.NotNull(resolved.Uri);
  424. var source = resolved.Uri.ToString();
  425. Assert.True(_modules.TryGetValue(source, out var script), $"Resolved module does not exist: {source}");
  426. return ModuleFactory.BuildSourceTextModule(engine, Engine.PrepareModule(script, source));
  427. }
  428. }
  429. private static string GetBasePath()
  430. {
  431. var assemblyDirectory = new DirectoryInfo(AppDomain.CurrentDomain.RelativeSearchPath ?? AppDomain.CurrentDomain.BaseDirectory);
  432. var current = assemblyDirectory;
  433. var binDirectory = $"{Path.DirectorySeparatorChar}bin{Path.DirectorySeparatorChar}";
  434. while (current is not null)
  435. {
  436. if (current.FullName.Contains(binDirectory) || current.Name == "bin")
  437. {
  438. current = current.Parent;
  439. continue;
  440. }
  441. var testDirectory = current.GetDirectories("Jint.Tests").FirstOrDefault();
  442. if (testDirectory == null)
  443. {
  444. current = current.Parent;
  445. continue;
  446. }
  447. // found it
  448. current = testDirectory;
  449. break;
  450. }
  451. if (current is null)
  452. {
  453. throw new NullReferenceException($"Could not find tests base path, assemblyPath: {assemblyDirectory}");
  454. }
  455. return Path.Combine(current.FullName, "Runtime", "Scripts");
  456. }
  457. [Fact]
  458. public void ModuleBuilderWithCustomModuleLoaderLoadsModulesProperly()
  459. {
  460. var engine = new Engine(o => o.EnableModules(new LocationResolveOnlyModuleLoader((_, moduleRequest) =>
  461. {
  462. var result = moduleRequest.Specifier;
  463. if (moduleRequest.Specifier == "../library1/builder_module.js")
  464. {
  465. result = "library1/builder_module.js";
  466. }
  467. return result;
  468. })));
  469. var logStatements = new List<string>();
  470. engine.SetValue("log", logStatements.Add);
  471. engine.Modules.Add("library1/builder_module.js",
  472. builder => builder.AddSource("export const value = 'builder_module_const'; log('builder_module')"));
  473. engine.Modules.Add("library2/entry_point_module.js",
  474. builder => builder.AddSource("import * as m from '../library1/builder_module.js'; log('entry_point_module')"));
  475. engine.Modules.Import("library2/entry_point_module.js");
  476. Assert.Collection(
  477. logStatements,
  478. s => Assert.Equal("builder_module", s),
  479. s => Assert.Equal("entry_point_module", s));
  480. }
  481. [Fact]
  482. public void ModuleBuilderPassesReferencingModuleLocationToModuleLoader()
  483. {
  484. var engine = new Engine(o => o.EnableModules(new LocationResolveOnlyModuleLoader((referencingModuleLocation, moduleRequest) =>
  485. {
  486. var result = moduleRequest.Specifier;
  487. if (moduleRequest.Specifier == "../library1/builder_module.js")
  488. {
  489. Assert.Equal("library2/entry_point_module.js", referencingModuleLocation);
  490. result = "library1/builder_module.js";
  491. }
  492. return result;
  493. })));
  494. var logStatements = new List<string>();
  495. engine.SetValue("log", logStatements.Add);
  496. engine.Modules.Add("library1/builder_module.js",
  497. builder => builder.AddSource("export const value = 'builder_module_const'; log('builder_module')"));
  498. engine.Modules.Add("library2/entry_point_module.js",
  499. builder => builder.AddSource("import * as m from '../library1/builder_module.js'; log('entry_point_module')"));
  500. engine.Modules.Import("library2/entry_point_module.js");
  501. Assert.Collection(
  502. logStatements,
  503. s => Assert.Equal("builder_module", s),
  504. s => Assert.Equal("entry_point_module", s));
  505. }
  506. /// <summary>
  507. /// Custom <see cref="ModuleLoader"/> implementation which is only responsible to
  508. /// resolve the correct module location (see <see cref="Resolve"/>). Modules
  509. /// must be registered using <see cref="Jint.Engine.ModuleOperations"/> (e.g.
  510. /// by using <see cref="Engine.ModuleOperations.Add(string,string)"/>)
  511. /// </summary>
  512. private sealed class LocationResolveOnlyModuleLoader : ModuleLoader
  513. {
  514. public delegate string ResolveHandler(string referencingModuleLocation, ModuleRequest moduleRequest);
  515. private readonly ResolveHandler _resolveHandler;
  516. public LocationResolveOnlyModuleLoader(ResolveHandler resolveHandler)
  517. {
  518. _resolveHandler = resolveHandler ?? throw new ArgumentNullException(nameof(resolveHandler));
  519. }
  520. public override ResolvedSpecifier Resolve(string referencingModuleLocation, ModuleRequest moduleRequest)
  521. {
  522. return new ResolvedSpecifier(
  523. moduleRequest,
  524. Key: _resolveHandler(referencingModuleLocation, moduleRequest),
  525. Uri: null,
  526. SpecifierType.RelativeOrAbsolute
  527. );
  528. }
  529. protected override string LoadModuleContents(Engine engine, ResolvedSpecifier resolved)
  530. => throw new InvalidOperationException();
  531. }
  532. [Fact]
  533. public void EngineShouldTransmitSourceModuleForModuleLoader()
  534. {
  535. var engine = new Engine(o => o.EnableModules(new ModuleLoaderForEngineShouldTransmitSourceModuleForModuleLoaderTest()));
  536. var logs = new List<string>();
  537. engine.SetValue("log", logs.Add);
  538. engine.Modules.Import($"code/lib/module.js");
  539. Assert.Collection(logs,
  540. s => Assert.Equal("code/execute.js", s),
  541. s => Assert.Equal("code/lib/module.js", s));
  542. }
  543. public class ModuleLoaderForEngineShouldTransmitSourceModuleForModuleLoaderTest : ModuleLoader
  544. {
  545. public override ResolvedSpecifier Resolve(string referencingModuleLocation, ModuleRequest moduleRequest)
  546. {
  547. var moduleSpec = moduleRequest.Specifier;
  548. // to resolve this statement requires information about source module
  549. if (moduleSpec == "../execute.js")
  550. {
  551. Assert.True(!string.IsNullOrEmpty(referencingModuleLocation), "module loader cannot resolve referensing module - has no referencing module location");
  552. moduleSpec = $"code/execute.js";
  553. }
  554. return new ResolvedSpecifier(
  555. moduleRequest,
  556. moduleSpec,
  557. Uri: null,
  558. SpecifierType.RelativeOrAbsolute
  559. );
  560. }
  561. protected override string LoadModuleContents(Engine engine, ResolvedSpecifier resolved)
  562. {
  563. if (resolved.Key == $"code/lib/module.js")
  564. return $"import * as m from '../execute.js'; log('code/lib/module.js')";
  565. if (resolved.Key == $"code/execute.js")
  566. {
  567. return $"log('code/execute.js')";
  568. }
  569. throw new NotImplementedException(); // no need in this test
  570. }
  571. }
  572. [Theory]
  573. [InlineData(false)]
  574. [InlineData(true)]
  575. public void CanStaticallyImportJsonModule(bool importViaLoader)
  576. {
  577. const string JsonModuleSpecifier = "./test.json";
  578. const string JsonModuleContent =
  579. """
  580. { "message": "hello" }
  581. """;
  582. const string MainModuleSpecifier = "./main.js";
  583. const string MainModuleCode =
  584. $$"""
  585. import json from "{{JsonModuleSpecifier}}" with { type: "json" };
  586. export const msg = json.message;
  587. """;
  588. var loaderModules = new Dictionary<string, Func<Engine, ResolvedSpecifier, Module>>();
  589. var engine = new Engine(o => o.EnableModules(new TestModuleLoader(loaderModules)));
  590. loaderModules.Add(JsonModuleSpecifier, (engine, resolved) => ModuleFactory.BuildJsonModule(engine, resolved, JsonModuleContent));
  591. if (importViaLoader)
  592. {
  593. loaderModules.Add(MainModuleSpecifier, (engine, resolved) => ModuleFactory.BuildSourceTextModule(engine, resolved, MainModuleCode));
  594. }
  595. else
  596. {
  597. engine.Modules.Add(MainModuleSpecifier, MainModuleCode);
  598. }
  599. var mainModule = engine.Modules.Import(MainModuleSpecifier);
  600. Assert.Equal("hello", mainModule.Get("msg").AsString());
  601. }
  602. [Theory]
  603. [InlineData(false)]
  604. [InlineData(true)]
  605. public async Task CanDynamicallyImportJsonModule(bool importViaLoader)
  606. {
  607. const string JsonModuleSpecifier = "./test.json";
  608. const string JsonModuleContent =
  609. """
  610. { "message": "hello" }
  611. """;
  612. const string MainModuleSpecifier = "./main.js";
  613. const string MainModuleCode =
  614. $$"""
  615. const json = await import("{{JsonModuleSpecifier}}", { with: { type: "json" } });
  616. callback(json.default.message);
  617. """;
  618. var completionTcs = new TaskCompletionSource<JsValue>(TaskCreationOptions.RunContinuationsAsynchronously);
  619. var loaderModules = new Dictionary<string, Func<Engine, ResolvedSpecifier, Module>>();
  620. var engine = new Engine(o => o.EnableModules(new TestModuleLoader(loaderModules)))
  621. .SetValue("callback", new Action<JsValue>(value => completionTcs.SetResult(value)));
  622. loaderModules.Add(JsonModuleSpecifier, (engine, resolved) => ModuleFactory.BuildJsonModule(engine, resolved, JsonModuleContent));
  623. if (importViaLoader)
  624. {
  625. loaderModules.Add(MainModuleSpecifier, (engine, resolved) => ModuleFactory.BuildSourceTextModule(engine, resolved, MainModuleCode));
  626. }
  627. else
  628. {
  629. engine.Modules.Add(MainModuleSpecifier, MainModuleCode);
  630. }
  631. var mainModule = engine.Modules.Import(MainModuleSpecifier);
  632. Assert.Equal("hello", (await completionTcs.Task).AsString());
  633. }
  634. private sealed class TestModuleLoader : IModuleLoader
  635. {
  636. private readonly Dictionary<string, Func<Engine, ResolvedSpecifier, Module>> _moduleFactories;
  637. public TestModuleLoader(Dictionary<string, Func<Engine, ResolvedSpecifier, Module>> moduleFactories)
  638. {
  639. _moduleFactories = moduleFactories;
  640. }
  641. ResolvedSpecifier IModuleLoader.Resolve(string referencingModuleLocation, ModuleRequest moduleRequest)
  642. {
  643. return new ResolvedSpecifier(moduleRequest, moduleRequest.Specifier, Uri: null, SpecifierType.RelativeOrAbsolute);
  644. }
  645. Module IModuleLoader.LoadModule(Engine engine, ResolvedSpecifier resolved)
  646. {
  647. if (_moduleFactories.TryGetValue(resolved.ModuleRequest.Specifier, out var moduleFactory))
  648. {
  649. return moduleFactory(engine, resolved);
  650. }
  651. throw new ArgumentException(null, nameof(resolved));
  652. }
  653. }
  654. }