ModuleLoaderTests.cs 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  1. using System.Collections.Concurrent;
  2. using Jint.Native;
  3. using Jint.Native.Json;
  4. using Jint.Runtime.Modules;
  5. #nullable enable
  6. namespace Jint.Tests.PublicInterface;
  7. public class ModuleLoaderTests
  8. {
  9. [Fact]
  10. public void CustomModuleLoaderWithUriModuleLocations()
  11. {
  12. // Dummy module store which shows that different protocols can be
  13. // used for modules.
  14. var store = new ModuleStore(new Dictionary<string, string>()
  15. {
  16. ["https://example.com/someModule.js"] = "export const DEFAULT_VALUE = 'remote';",
  17. ["https://example.com/test.js"] = "import { DEFAULT_VALUE } from 'someModule.js'; export const value = DEFAULT_VALUE;",
  18. ["file:///someModule.js"] = "export const value = 'local';",
  19. ["proprietary-protocol:///someModule.js"] = "export const value = 'proprietary';",
  20. });
  21. var sharedModules = new CachedModuleLoader(store);
  22. var runA = RunModule("import { value } from 'https://example.com/test.js'; log(value);");
  23. var runB = RunModule("import { value } from 'someModule.js'; log(value);");
  24. var runC = RunModule("import { value } from 'proprietary-protocol:///someModule.js'; log(value);");
  25. ExpectLoggedValue(runA, "remote");
  26. ExpectLoggedValue(runB, "local");
  27. ExpectLoggedValue(runC, "proprietary");
  28. static void ExpectLoggedValue(ModuleScript executedScript, string expectedValue)
  29. {
  30. Assert.Single(executedScript.Logs);
  31. Assert.Equal(expectedValue, executedScript.Logs[0]);
  32. }
  33. ModuleScript RunModule(string code)
  34. {
  35. var result = new ModuleScript(code, sharedModules);
  36. result.Execute();
  37. return result;
  38. }
  39. }
  40. [Fact]
  41. public void CustomModuleLoaderWithCachingSupport()
  42. {
  43. // Different engines use the same module loader.
  44. // The module loader caches the parsed Esprima.Ast.Module
  45. // which allows to re-use these for different engine runs.
  46. var store = new ModuleStore(new Dictionary<string, string>()
  47. {
  48. ["file:///localModule.js"] = "export const value = 'local';",
  49. });
  50. var sharedModules = new CachedModuleLoader(store);
  51. // Simulate the re-use by simply running the same main entry point 10 times.
  52. foreach (var _ in Enumerable.Range(0, 10))
  53. {
  54. var runner = new ModuleScript("import { value } from 'localModule.js'; log(value);", sharedModules);
  55. runner.Execute();
  56. }
  57. Assert.Equal(1, sharedModules.ModulesParsed);
  58. }
  59. [Fact]
  60. public void CustomModuleLoaderCanWorkWithJsonModules()
  61. {
  62. var store = new ModuleStore(new Dictionary<string, string>()
  63. {
  64. ["file:///config.json"] = "{ \"value\": \"json\" }",
  65. });
  66. var sharedModules = new CachedModuleLoader(store);
  67. var runner = new ModuleScript("import data from 'config.json' with { type: 'json' }; log(data.value);", sharedModules);
  68. runner.Execute();
  69. Assert.Single(runner.Logs);
  70. Assert.Equal("json", runner.Logs[0]);
  71. }
  72. /// <summary>
  73. /// A simple in-memory store for module sources. The keys
  74. /// must be absolute <see cref="Uri.ToString()"/> values.
  75. /// </summary>
  76. /// <remarks>
  77. /// This is just an example and not production ready code. The implementation
  78. /// is missing important path traversal checks and other edge cases.
  79. /// </remarks>
  80. private sealed class ModuleStore
  81. {
  82. private const string DefaultProtocol = "file:///./";
  83. private readonly IReadOnlyDictionary<string, string> _sourceCode;
  84. public ModuleStore(IReadOnlyDictionary<string, string> sourceCode)
  85. {
  86. _sourceCode = sourceCode;
  87. }
  88. public ResolvedSpecifier Resolve(string? referencingModuleLocation, ModuleRequest moduleRequest)
  89. {
  90. Uri uri = Resolve(referencingModuleLocation, moduleRequest.Specifier);
  91. return new ResolvedSpecifier(moduleRequest, uri.ToString(), uri, SpecifierType.Bare);
  92. }
  93. private Uri Resolve(string? referencingModuleLocation, string specifier)
  94. {
  95. if (Uri.TryCreate(specifier, UriKind.Absolute, out Uri? absoluteLocation))
  96. return absoluteLocation;
  97. if (!string.IsNullOrEmpty(referencingModuleLocation) && Uri.TryCreate(referencingModuleLocation, UriKind.Absolute, out Uri? baseUri))
  98. {
  99. if (Uri.TryCreate(baseUri, specifier, out Uri? relative))
  100. return relative;
  101. }
  102. return new Uri(DefaultProtocol + specifier);
  103. }
  104. public string GetModuleSource(Uri uri)
  105. {
  106. if (!_sourceCode.TryGetValue(uri.ToString(), out var result))
  107. throw new InvalidOperationException($"Module not found: {uri}");
  108. return result;
  109. }
  110. }
  111. /// <summary>
  112. /// The main entry point for a module script. Allows
  113. /// to use a script as a main module.
  114. /// </summary>
  115. private sealed class ModuleScript : IModuleLoader
  116. {
  117. private const string MainSpecifier = "____main____";
  118. private readonly List<string> _logs = new();
  119. private readonly Engine _engine;
  120. private readonly string _main;
  121. private readonly IModuleLoader _modules;
  122. public ModuleScript(string main, IModuleLoader modules)
  123. {
  124. _main = main;
  125. _modules = modules;
  126. _engine = new Engine(options => options.EnableModules(this));
  127. _engine.SetValue("log", _logs.Add);
  128. }
  129. public IReadOnlyList<string> Logs => _logs;
  130. public void Execute()
  131. {
  132. _engine.Modules.Import(MainSpecifier);
  133. }
  134. ResolvedSpecifier IModuleLoader.Resolve(string? referencingModuleLocation, ModuleRequest moduleRequest)
  135. {
  136. if (moduleRequest.Specifier == MainSpecifier)
  137. return new ResolvedSpecifier(moduleRequest, MainSpecifier, null, SpecifierType.Bare);
  138. return _modules.Resolve(referencingModuleLocation, moduleRequest);
  139. }
  140. Module IModuleLoader.LoadModule(Engine engine, ResolvedSpecifier resolved)
  141. {
  142. if (resolved.ModuleRequest.Specifier == MainSpecifier)
  143. return ModuleFactory.BuildSourceTextModule(engine, Engine.PrepareModule(_main, MainSpecifier));
  144. return _modules.LoadModule(engine, resolved);
  145. }
  146. }
  147. /// <summary>
  148. /// <para>
  149. /// A simple <see cref="IModuleLoader"/> implementation which will
  150. /// re-use prepared <see cref="Esprima.Ast.Module"/> or <see cref="JsValue"/> modules to
  151. /// produce <see cref="Jint.Runtime.Modules.Module"/>.
  152. /// </para>
  153. /// <para>
  154. /// The module source gets loaded from <see cref="ModuleStore"/>.
  155. /// </para>
  156. /// </summary>
  157. private sealed class CachedModuleLoader : IModuleLoader
  158. {
  159. private readonly ConcurrentDictionary<Uri, ParsedModule> _parsedModules = new();
  160. private readonly ModuleStore _store;
  161. #if NETCOREAPP1_0_OR_GREATER
  162. private readonly Func<Uri, ResolvedSpecifier, ParsedModule> _moduleParser;
  163. #endif
  164. private int _modulesParsed;
  165. public CachedModuleLoader(ModuleStore store)
  166. {
  167. _store = store;
  168. #if NETCOREAPP1_0_OR_GREATER
  169. _moduleParser = GetParsedModule;
  170. #endif
  171. }
  172. public int ModulesParsed => _modulesParsed;
  173. public ResolvedSpecifier Resolve(string? referencingModuleLocation, ModuleRequest moduleRequest)
  174. {
  175. return _store.Resolve(referencingModuleLocation, moduleRequest);
  176. }
  177. public Module LoadModule(Engine engine, ResolvedSpecifier resolved)
  178. {
  179. Assert.NotNull(resolved.Uri);
  180. #if NETCOREAPP1_0_OR_GREATER
  181. var parsedModule = _parsedModules.GetOrAdd(resolved.Uri, _moduleParser, resolved);
  182. #else
  183. var parsedModule = _parsedModules.GetOrAdd(resolved.Uri, _ => GetParsedModule(resolved.Uri, resolved));
  184. #endif
  185. return parsedModule.ToModule(engine);
  186. }
  187. private ParsedModule GetParsedModule(Uri uri, ResolvedSpecifier resolved)
  188. {
  189. var script = _store.GetModuleSource(resolved.Uri!);
  190. var result = resolved.ModuleRequest.IsJsonModule()
  191. ? ParsedModule.JsonModule(script, resolved.Uri!.ToString())
  192. : ParsedModule.TextModule(script, resolved.Uri!.ToString());
  193. Interlocked.Increment(ref _modulesParsed);
  194. return result;
  195. }
  196. private sealed class ParsedModule
  197. {
  198. private readonly Esprima.Ast.Module? _textModule;
  199. private readonly (JsValue Json, string Location)? _jsonModule;
  200. private ParsedModule(Esprima.Ast.Module? textModule, (JsValue Json, string Location)? jsonModule)
  201. {
  202. _textModule = textModule;
  203. _jsonModule = jsonModule;
  204. }
  205. public static ParsedModule TextModule(string script, string location)
  206. => new(Engine.PrepareModule(script, location), null);
  207. public static ParsedModule JsonModule(string json, string location)
  208. => new(null, (ParseJson(json), location));
  209. private static JsValue ParseJson(string json)
  210. {
  211. var engine = new Engine();
  212. var parser = new JsonParser(engine);
  213. return parser.Parse(json);
  214. }
  215. public Module ToModule(Engine engine)
  216. {
  217. if (_jsonModule is not null)
  218. return ModuleFactory.BuildJsonModule(engine, _jsonModule.Value.Json, _jsonModule.Value.Location);
  219. if (_textModule is not null)
  220. return ModuleFactory.BuildSourceTextModule(engine, _textModule);
  221. throw new InvalidOperationException("Unexpected state - no module type available");
  222. }
  223. }
  224. }
  225. }