using Esprima;
using Esprima.Ast;
namespace Jint.Runtime.Modules;
public sealed class DefaultModuleLoader : IModuleLoader
{
public delegate void ModuleLoadedEventHandler(object sender, string source, Module module);
private readonly Uri _basePath;
private readonly bool _restrictToBasePath;
///
/// The Loaded event is triggered after a module is loaded and parsed.
///
///
/// The event is not triggered if a module was loaded but failed to parse.
///
public event ModuleLoadedEventHandler? Loaded;
public DefaultModuleLoader(string basePath) : this(basePath, true)
{
}
public DefaultModuleLoader(string basePath, bool restrictToBasePath)
{
if (string.IsNullOrWhiteSpace(basePath))
{
ExceptionHelper.ThrowArgumentException("Value cannot be null or whitespace.", nameof(basePath));
}
_restrictToBasePath = restrictToBasePath;
if (!Uri.TryCreate(basePath, UriKind.Absolute, out _basePath))
{
if (!Path.IsPathRooted(basePath))
{
ExceptionHelper.ThrowArgumentException("Path must be rooted", nameof(basePath));
}
basePath = Path.GetFullPath(basePath);
_basePath = new Uri(basePath, UriKind.Absolute);
}
if (_basePath.AbsolutePath[_basePath.AbsolutePath.Length - 1] != '/')
{
var uriBuilder = new UriBuilder(_basePath);
uriBuilder.Path += '/';
_basePath = uriBuilder.Uri;
}
}
public ResolvedSpecifier Resolve(string? referencingModuleLocation, string specifier)
{
if (string.IsNullOrEmpty(specifier))
{
ExceptionHelper.ThrowModuleResolutionException("Invalid Module Specifier", specifier, referencingModuleLocation);
return default;
}
// Specifications from ESM_RESOLVE Algorithm: https://nodejs.org/api/esm.html#resolution-algorithm
Uri resolved;
if (Uri.TryCreate(specifier, UriKind.Absolute, out var uri))
{
resolved = uri;
}
else if (IsRelative(specifier))
{
resolved = new Uri(referencingModuleLocation != null ? new Uri(referencingModuleLocation, UriKind.Absolute) : _basePath, specifier);
}
else if (specifier[0] == '#')
{
ExceptionHelper.ThrowNotSupportedException($"PACKAGE_IMPORTS_RESOLVE is not supported: '{specifier}'");
return default;
}
else
{
return new ResolvedSpecifier(
specifier,
specifier,
null,
SpecifierType.Bare
);
}
if (resolved.IsFile)
{
if (resolved.UserEscaped)
{
ExceptionHelper.ThrowModuleResolutionException("Invalid Module Specifier", specifier, referencingModuleLocation);
return default;
}
if (!Path.HasExtension(resolved.LocalPath))
{
ExceptionHelper.ThrowModuleResolutionException("Unsupported Directory Import", specifier, referencingModuleLocation);
return default;
}
}
if (_restrictToBasePath && !_basePath.IsBaseOf(resolved))
{
ExceptionHelper.ThrowModuleResolutionException($"Unauthorized Module Path", specifier, referencingModuleLocation);
return default;
}
return new ResolvedSpecifier(
specifier,
resolved.AbsoluteUri,
resolved,
SpecifierType.RelativeOrAbsolute
);
}
public Module LoadModule(Engine engine, ResolvedSpecifier resolved)
{
if (resolved.Type != SpecifierType.RelativeOrAbsolute)
{
ExceptionHelper.ThrowNotSupportedException($"The default module loader can only resolve files. You can define modules directly to allow imports using {nameof(Engine)}.{nameof(Engine.AddModule)}(). Attempted to resolve: '{resolved.Specifier}'.");
return default;
}
if (resolved.Uri == null)
{
ExceptionHelper.ThrowInvalidOperationException($"Module '{resolved.Specifier}' of type '{resolved.Type}' has no resolved URI.");
}
var fileName = Uri.UnescapeDataString(resolved.Uri.AbsolutePath);
if (!File.Exists(fileName))
{
ExceptionHelper.ThrowArgumentException("Module Not Found: ", resolved.Specifier);
return default;
}
var code = File.ReadAllText(fileName);
var source = resolved.Uri.LocalPath;
Module module;
try
{
module = new JavaScriptParser().ParseModule(code, source);
}
catch (ParserException ex)
{
ExceptionHelper.ThrowSyntaxError(engine.Realm, $"Error while loading module: error in module '{source}': {ex.Error}");
module = null;
}
catch (Exception)
{
ExceptionHelper.ThrowJavaScriptException(engine, $"Could not load module {source}", (Location) default);
module = null;
}
Loaded?.Invoke(this, source, module);
return module;
}
private static bool IsRelative(string specifier)
{
return specifier.StartsWith(".") || specifier.StartsWith("/");
}
}