ModuleTests.cs 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780
  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([
  188. "act_noargs",
  189. "act_args:ok",
  190. "fn_noargs",
  191. "fn_args:ok"
  192. ], received.ToArray());
  193. Assert.Equal([
  194. "undefined",
  195. "undefined",
  196. "ret",
  197. "ret"
  198. ], ns.Get("result").AsArray().Select(x => x.ToString()).ToArray());
  199. }
  200. private class ImportedClass
  201. {
  202. public string Value { get; set; } = "hello world";
  203. }
  204. [Fact]
  205. public void ShouldAllowExportMultipleImports()
  206. {
  207. _engine.Modules.Add("@mine/import1", builder => builder.ExportValue("value1", JsNumber.Create(1)));
  208. _engine.Modules.Add("@mine/import2", builder => builder.ExportValue("value2", JsNumber.Create(2)));
  209. _engine.Modules.Add("@mine", "export * from '@mine/import1'; export * from '@mine/import2'");
  210. _engine.Modules.Add("app", "import { value1, value2 } from '@mine'; export const result = `${value1} ${value2}`");
  211. var ns = _engine.Modules.Import("app");
  212. Assert.Equal("1 2", ns.Get("result").AsString());
  213. }
  214. [Fact]
  215. public void ShouldAllowNamedStarExport()
  216. {
  217. _engine.Modules.Add("imported-module", builder => builder.ExportValue("value1", 5));
  218. _engine.Modules.Add("my-module", "export * as ns from 'imported-module';");
  219. var ns = _engine.Modules.Import("my-module");
  220. Assert.Equal(5, ns.Get("ns").Get("value1").AsNumber());
  221. }
  222. [Fact]
  223. public void ShouldAllowChaining()
  224. {
  225. _engine.Modules.Add("dependent-module", "export const dependency = 1;");
  226. _engine.Modules.Add("my-module", builder => builder
  227. .AddSource("import { dependency } from 'dependent-module';")
  228. .AddSource("export const output = dependency + 1;")
  229. .ExportValue("num", JsNumber.Create(-1))
  230. );
  231. var ns = _engine.Modules.Import("my-module");
  232. Assert.Equal(2, ns.Get("output").AsInteger());
  233. Assert.Equal(-1, ns.Get("num").AsInteger());
  234. }
  235. [Fact]
  236. public void ShouldImportOnlyOnce()
  237. {
  238. var called = 0;
  239. _engine.Modules.Add("imported-module", builder => builder.ExportFunction("count", args => called++));
  240. _engine.Modules.Add("my-module", "import { count } from 'imported-module'; count();");
  241. _engine.Modules.Import("my-module");
  242. _engine.Modules.Import("my-module");
  243. Assert.Equal(1, called);
  244. }
  245. [Fact]
  246. public void ShouldAllowSelfImport()
  247. {
  248. _engine.Modules.Add("my-globals", "export const globals = { counter: 0 };");
  249. _engine.Modules.Add("my-module", @"
  250. import { globals } from 'my-globals';
  251. import {} from 'my-module';
  252. globals.counter++;
  253. export const count = globals.counter;
  254. ");
  255. var ns = _engine.Modules.Import("my-module");
  256. Assert.Equal(1, ns.Get("count").AsInteger());
  257. }
  258. [Fact]
  259. public void ShouldAllowCyclicImport()
  260. {
  261. // https://tc39.es/ecma262/#sec-example-cyclic-module-record-graphs
  262. _engine.Modules.Add("B", "import { a } from 'A'; export const b = 'b';");
  263. _engine.Modules.Add("A", "import { b } from 'B'; export const a = 'a';");
  264. var nsA = _engine.Modules.Import("A");
  265. var nsB = _engine.Modules.Import("B");
  266. Assert.Equal("a", nsA.Get("a").AsString());
  267. Assert.Equal("b", nsB.Get("b").AsString());
  268. }
  269. [Fact]
  270. public void ShouldSupportConstraints()
  271. {
  272. var engine = new Engine(opts => opts.TimeoutInterval(TimeSpan.FromTicks(1)));
  273. engine.Modules.Add("sleep", builder => builder.ExportFunction("sleep", () => Thread.Sleep(100)));
  274. engine.Modules.Add("my-module", "import { sleep } from 'sleep'; for(var i = 0; i < 100; i++) { sleep(); } export const result = 'ok';");
  275. Assert.Throws<TimeoutException>(() => engine.Modules.Import("my-module"));
  276. }
  277. [Fact]
  278. public void CanLoadModuleImportsFromFiles()
  279. {
  280. var engine = new Engine(options => options.EnableModules(GetBasePath()));
  281. engine.Modules.Add("my-module", "import { User } from './modules/user.js'; export const user = new User('John', 'Doe');");
  282. var ns = engine.Modules.Import("my-module");
  283. Assert.Equal("John Doe", ns["user"].Get("name").AsString());
  284. }
  285. [Fact]
  286. public void CanImportFromFile()
  287. {
  288. var engine = new Engine(options => options.EnableModules(GetBasePath()));
  289. var ns = engine.Modules.Import("./modules/format-name.js");
  290. var result = engine.Invoke(ns.Get("formatName"), "John", "Doe").AsString();
  291. Assert.Equal("John Doe", result);
  292. }
  293. [Fact]
  294. public void CanImportFromFileWithSpacesInPath()
  295. {
  296. var engine = new Engine(options => options.EnableModules(GetBasePath()));
  297. var ns = engine.Modules.Import("./dir with spaces/format name.js");
  298. var result = engine.Invoke(ns.Get("formatName"), "John", "Doe").AsString();
  299. Assert.Equal("John Doe", result);
  300. }
  301. [Fact]
  302. public void CanReuseModule()
  303. {
  304. const string Code = "export function formatName(firstName, lastName) {\r\n return `${firstName} ${lastName}`;\r\n}";
  305. var module = Engine.PrepareModule(Code);
  306. for (var i = 0; i < 5; i++)
  307. {
  308. var engine = new Engine();
  309. engine.Modules.Add("__main__", x => x.AddModule(module));
  310. var ns = engine.Modules.Import("__main__");
  311. var result = engine.Invoke(ns.Get("formatName"), "John" + i, "Doe").AsString();
  312. Assert.Equal($"John{i} Doe", result);
  313. }
  314. }
  315. [Fact]
  316. public void EngineExecutePassesSourceForModuleResolving()
  317. {
  318. var moduleLoader = new EnforceRelativeModuleLoader(new Dictionary<string, string>()
  319. {
  320. ["file:///folder/my-module.js"] = "export const value = 'myModuleConst'"
  321. });
  322. var engine = new Engine(options => options.EnableModules(moduleLoader));
  323. var code = @"
  324. (async () => {
  325. const { value } = await import('./my-module.js');
  326. log(value);
  327. })();
  328. ";
  329. List<string> logStatements = [];
  330. engine.SetValue("log", logStatements.Add);
  331. engine.Execute(code, source: "file:///folder/main.js");
  332. engine.Advanced.ProcessTasks();
  333. Assert.Collection(
  334. logStatements,
  335. s => Assert.Equal("myModuleConst", s));
  336. }
  337. [Fact]
  338. public void EngineExecuteUsesScriptSourceForSource()
  339. {
  340. var moduleLoader = new EnforceRelativeModuleLoader(new Dictionary<string, string>()
  341. {
  342. ["file:///folder/my-module.js"] = "export const value = 'myModuleConst'"
  343. });
  344. var engine = new Engine(options => options.EnableModules(moduleLoader));
  345. var code = @"
  346. (async () => {
  347. const { value } = await import('./my-module.js');
  348. log(value);
  349. })();
  350. ";
  351. List<string> logStatements = [];
  352. engine.SetValue("log", logStatements.Add);
  353. var script = Engine.PrepareScript(code, source: "file:///folder/main.js");
  354. engine.Execute(script);
  355. engine.Advanced.ProcessTasks();
  356. Assert.Collection(
  357. logStatements,
  358. s => Assert.Equal("myModuleConst", s));
  359. }
  360. [Fact]
  361. public void EngineEvaluatePassesSourceForModuleResolving()
  362. {
  363. var moduleLoader = new EnforceRelativeModuleLoader(new Dictionary<string, string>()
  364. {
  365. ["file:///folder/my-module.js"] = "export const value = 'myModuleConst'"
  366. });
  367. var engine = new Engine(options => options.EnableModules(moduleLoader));
  368. var code = @"
  369. (async () => {
  370. const { value } = await import('./my-module.js');
  371. log(value);
  372. })();
  373. ";
  374. List<string> logStatements = [];
  375. engine.SetValue("log", logStatements.Add);
  376. engine.Evaluate(code, source: "file:///folder/main.js");
  377. engine.Advanced.ProcessTasks();
  378. Assert.Collection(
  379. logStatements,
  380. s => Assert.Equal("myModuleConst", s));
  381. }
  382. [Fact]
  383. public void EngineEvaluateUsesScriptSourceForSource()
  384. {
  385. var moduleLoader = new EnforceRelativeModuleLoader(new Dictionary<string, string>()
  386. {
  387. ["file:///folder/my-module.js"] = "export const value = 'myModuleConst'"
  388. });
  389. var engine = new Engine(options => options.EnableModules(moduleLoader));
  390. var code = @"
  391. (async () => {
  392. const { value } = await import('./my-module.js');
  393. log(value);
  394. })();
  395. ";
  396. List<string> logStatements = [];
  397. engine.SetValue("log", logStatements.Add);
  398. var script = Engine.PrepareScript(code, source: "file:///folder/main.js");
  399. engine.Evaluate(script);
  400. engine.Advanced.ProcessTasks();
  401. Assert.Collection(
  402. logStatements,
  403. s => Assert.Equal("myModuleConst", s));
  404. }
  405. private sealed class EnforceRelativeModuleLoader : IModuleLoader
  406. {
  407. private readonly IReadOnlyDictionary<string, string> _modules;
  408. public EnforceRelativeModuleLoader(IReadOnlyDictionary<string, string> modules)
  409. {
  410. _modules = modules;
  411. }
  412. public ResolvedSpecifier Resolve(string referencingModuleLocation, ModuleRequest moduleRequest)
  413. {
  414. Assert.False(string.IsNullOrEmpty(referencingModuleLocation), "Referencing module location is null or empty");
  415. var target = new Uri(new Uri(referencingModuleLocation, UriKind.Absolute), moduleRequest.Specifier);
  416. Assert.True(_modules.ContainsKey(target.ToString()), $"Resolve was called with unexpected module request, {moduleRequest.Specifier} relative to {referencingModuleLocation}");
  417. return new ResolvedSpecifier(moduleRequest, target.ToString(), target, SpecifierType.Bare);
  418. }
  419. public Module LoadModule(Engine engine, ResolvedSpecifier resolved)
  420. {
  421. Assert.NotNull(resolved.Uri);
  422. var source = resolved.Uri.ToString();
  423. Assert.True(_modules.TryGetValue(source, out var script), $"Resolved module does not exist: {source}");
  424. return ModuleFactory.BuildSourceTextModule(engine, Engine.PrepareModule(script, source));
  425. }
  426. }
  427. private static string GetBasePath()
  428. {
  429. var assemblyDirectory = new DirectoryInfo(AppDomain.CurrentDomain.RelativeSearchPath ?? AppDomain.CurrentDomain.BaseDirectory);
  430. var current = assemblyDirectory;
  431. var binDirectory = $"{Path.DirectorySeparatorChar}bin{Path.DirectorySeparatorChar}";
  432. while (current is not null)
  433. {
  434. if (current.FullName.Contains(binDirectory) || current.Name == "bin")
  435. {
  436. current = current.Parent;
  437. continue;
  438. }
  439. var testDirectory = current.GetDirectories("Jint.Tests").FirstOrDefault();
  440. if (testDirectory == null)
  441. {
  442. current = current.Parent;
  443. continue;
  444. }
  445. // found it
  446. current = testDirectory;
  447. break;
  448. }
  449. if (current is null)
  450. {
  451. throw new NullReferenceException($"Could not find tests base path, assemblyPath: {assemblyDirectory}");
  452. }
  453. return Path.Combine(current.FullName, "Runtime", "Scripts");
  454. }
  455. [Fact]
  456. public void ModuleBuilderWithCustomModuleLoaderLoadsModulesProperly()
  457. {
  458. var engine = new Engine(o => o.EnableModules(new LocationResolveOnlyModuleLoader((_, moduleRequest) =>
  459. {
  460. var result = moduleRequest.Specifier;
  461. if (moduleRequest.Specifier == "../library1/builder_module.js")
  462. {
  463. result = "library1/builder_module.js";
  464. }
  465. return result;
  466. })));
  467. var logStatements = new List<string>();
  468. engine.SetValue("log", logStatements.Add);
  469. engine.Modules.Add("library1/builder_module.js",
  470. builder => builder.AddSource("export const value = 'builder_module_const'; log('builder_module')"));
  471. engine.Modules.Add("library2/entry_point_module.js",
  472. builder => builder.AddSource("import * as m from '../library1/builder_module.js'; log('entry_point_module')"));
  473. engine.Modules.Import("library2/entry_point_module.js");
  474. Assert.Collection(
  475. logStatements,
  476. s => Assert.Equal("builder_module", s),
  477. s => Assert.Equal("entry_point_module", s));
  478. }
  479. [Fact]
  480. public void ModuleBuilderPassesReferencingModuleLocationToModuleLoader()
  481. {
  482. var engine = new Engine(o => o.EnableModules(new LocationResolveOnlyModuleLoader((referencingModuleLocation, moduleRequest) =>
  483. {
  484. var result = moduleRequest.Specifier;
  485. if (moduleRequest.Specifier == "../library1/builder_module.js")
  486. {
  487. Assert.Equal("library2/entry_point_module.js", referencingModuleLocation);
  488. result = "library1/builder_module.js";
  489. }
  490. return result;
  491. })));
  492. var logStatements = new List<string>();
  493. engine.SetValue("log", logStatements.Add);
  494. engine.Modules.Add("library1/builder_module.js",
  495. builder => builder.AddSource("export const value = 'builder_module_const'; log('builder_module')"));
  496. engine.Modules.Add("library2/entry_point_module.js",
  497. builder => builder.AddSource("import * as m from '../library1/builder_module.js'; log('entry_point_module')"));
  498. engine.Modules.Import("library2/entry_point_module.js");
  499. Assert.Collection(
  500. logStatements,
  501. s => Assert.Equal("builder_module", s),
  502. s => Assert.Equal("entry_point_module", s));
  503. }
  504. /// <summary>
  505. /// Custom <see cref="ModuleLoader"/> implementation which is only responsible to
  506. /// resolve the correct module location (see <see cref="Resolve"/>). Modules
  507. /// must be registered using <see cref="Jint.Engine.ModuleOperations"/> (e.g.
  508. /// by using <see cref="Engine.ModuleOperations.Add(string,string)"/>)
  509. /// </summary>
  510. private sealed class LocationResolveOnlyModuleLoader : ModuleLoader
  511. {
  512. public delegate string ResolveHandler(string referencingModuleLocation, ModuleRequest moduleRequest);
  513. private readonly ResolveHandler _resolveHandler;
  514. public LocationResolveOnlyModuleLoader(ResolveHandler resolveHandler)
  515. {
  516. _resolveHandler = resolveHandler ?? throw new ArgumentNullException(nameof(resolveHandler));
  517. }
  518. public override ResolvedSpecifier Resolve(string referencingModuleLocation, ModuleRequest moduleRequest)
  519. {
  520. return new ResolvedSpecifier(
  521. moduleRequest,
  522. Key: _resolveHandler(referencingModuleLocation, moduleRequest),
  523. Uri: null,
  524. SpecifierType.RelativeOrAbsolute
  525. );
  526. }
  527. protected override string LoadModuleContents(Engine engine, ResolvedSpecifier resolved)
  528. => throw new InvalidOperationException();
  529. }
  530. [Fact]
  531. public void EngineShouldTransmitSourceModuleForModuleLoader()
  532. {
  533. var engine = new Engine(o => o.EnableModules(new ModuleLoaderForEngineShouldTransmitSourceModuleForModuleLoaderTest()));
  534. var logs = new List<string>();
  535. engine.SetValue("log", logs.Add);
  536. engine.Modules.Import($"code/lib/module.js");
  537. Assert.Collection(logs,
  538. s => Assert.Equal("code/execute.js", s),
  539. s => Assert.Equal("code/lib/module.js", s));
  540. }
  541. public class ModuleLoaderForEngineShouldTransmitSourceModuleForModuleLoaderTest : ModuleLoader
  542. {
  543. public override ResolvedSpecifier Resolve(string referencingModuleLocation, ModuleRequest moduleRequest)
  544. {
  545. var moduleSpec = moduleRequest.Specifier;
  546. // to resolve this statement requires information about source module
  547. if (moduleSpec == "../execute.js")
  548. {
  549. Assert.True(!string.IsNullOrEmpty(referencingModuleLocation), "module loader cannot resolve referensing module - has no referencing module location");
  550. moduleSpec = $"code/execute.js";
  551. }
  552. return new ResolvedSpecifier(
  553. moduleRequest,
  554. moduleSpec,
  555. Uri: null,
  556. SpecifierType.RelativeOrAbsolute
  557. );
  558. }
  559. protected override string LoadModuleContents(Engine engine, ResolvedSpecifier resolved)
  560. {
  561. if (resolved.Key == $"code/lib/module.js")
  562. return $"import * as m from '../execute.js'; log('code/lib/module.js')";
  563. if (resolved.Key == $"code/execute.js")
  564. {
  565. return $"log('code/execute.js')";
  566. }
  567. throw new NotImplementedException(); // no need in this test
  568. }
  569. }
  570. [Theory]
  571. [InlineData(false)]
  572. [InlineData(true)]
  573. public void CanStaticallyImportJsonModule(bool importViaLoader)
  574. {
  575. const string JsonModuleSpecifier = "./test.json";
  576. const string JsonModuleContent =
  577. """
  578. { "message": "hello" }
  579. """;
  580. const string MainModuleSpecifier = "./main.js";
  581. const string MainModuleCode =
  582. $$"""
  583. import json from "{{JsonModuleSpecifier}}" with { type: "json" };
  584. export const msg = json.message;
  585. """;
  586. var loaderModules = new Dictionary<string, Func<Engine, ResolvedSpecifier, Module>>();
  587. var engine = new Engine(o => o.EnableModules(new TestModuleLoader(loaderModules)));
  588. loaderModules.Add(JsonModuleSpecifier, (engine, resolved) => ModuleFactory.BuildJsonModule(engine, resolved, JsonModuleContent));
  589. if (importViaLoader)
  590. {
  591. loaderModules.Add(MainModuleSpecifier, (engine, resolved) => ModuleFactory.BuildSourceTextModule(engine, resolved, MainModuleCode));
  592. }
  593. else
  594. {
  595. engine.Modules.Add(MainModuleSpecifier, MainModuleCode);
  596. }
  597. var mainModule = engine.Modules.Import(MainModuleSpecifier);
  598. Assert.Equal("hello", mainModule.Get("msg").AsString());
  599. }
  600. [Theory]
  601. [InlineData(false)]
  602. [InlineData(true)]
  603. public async Task CanDynamicallyImportJsonModule(bool importViaLoader)
  604. {
  605. const string JsonModuleSpecifier = "./test.json";
  606. const string JsonModuleContent =
  607. """
  608. { "message": "hello" }
  609. """;
  610. const string MainModuleSpecifier = "./main.js";
  611. const string MainModuleCode =
  612. $$"""
  613. const json = await import("{{JsonModuleSpecifier}}", { with: { type: "json" } });
  614. callback(json.default.message);
  615. """;
  616. var completionTcs = new TaskCompletionSource<JsValue>(TaskCreationOptions.RunContinuationsAsynchronously);
  617. var loaderModules = new Dictionary<string, Func<Engine, ResolvedSpecifier, Module>>();
  618. var engine = new Engine(o => o.EnableModules(new TestModuleLoader(loaderModules)))
  619. .SetValue("callback", new Action<JsValue>(value => completionTcs.SetResult(value)));
  620. loaderModules.Add(JsonModuleSpecifier, (engine, resolved) => ModuleFactory.BuildJsonModule(engine, resolved, JsonModuleContent));
  621. if (importViaLoader)
  622. {
  623. loaderModules.Add(MainModuleSpecifier, (engine, resolved) => ModuleFactory.BuildSourceTextModule(engine, resolved, MainModuleCode));
  624. }
  625. else
  626. {
  627. engine.Modules.Add(MainModuleSpecifier, MainModuleCode);
  628. }
  629. var mainModule = engine.Modules.Import(MainModuleSpecifier);
  630. Assert.Equal("hello", (await completionTcs.Task).AsString());
  631. }
  632. private sealed class TestModuleLoader : IModuleLoader
  633. {
  634. private readonly Dictionary<string, Func<Engine, ResolvedSpecifier, Module>> _moduleFactories;
  635. public TestModuleLoader(Dictionary<string, Func<Engine, ResolvedSpecifier, Module>> moduleFactories)
  636. {
  637. _moduleFactories = moduleFactories;
  638. }
  639. ResolvedSpecifier IModuleLoader.Resolve(string referencingModuleLocation, ModuleRequest moduleRequest)
  640. {
  641. return new ResolvedSpecifier(moduleRequest, moduleRequest.Specifier, Uri: null, SpecifierType.RelativeOrAbsolute);
  642. }
  643. Module IModuleLoader.LoadModule(Engine engine, ResolvedSpecifier resolved)
  644. {
  645. if (_moduleFactories.TryGetValue(resolved.ModuleRequest.Specifier, out var moduleFactory))
  646. {
  647. return moduleFactory(engine, resolved);
  648. }
  649. throw new ArgumentException(null, nameof(resolved));
  650. }
  651. }
  652. }