ModuleLoaderTests.cs 9.9 KB

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