Initial commit
This commit is contained in:
@@ -0,0 +1,238 @@
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.JSInterop;
|
||||
using RobotNet10.ScriptEditor.Helpers.Code;
|
||||
using RobotNet10.ScriptEditor.Helpers.Webcil;
|
||||
|
||||
namespace RobotNet10.ScriptEditor.Services;
|
||||
|
||||
public class ScriptResourceResolver
|
||||
{
|
||||
private readonly HttpClient httpClient;
|
||||
private readonly IJSRuntime? jsRuntime;
|
||||
private readonly Lazy<Task<Dictionary<string, string>>> _resourceMappings;
|
||||
|
||||
public ScriptResourceResolver(HttpClient client, IJSRuntime? jsRuntime = null)
|
||||
{
|
||||
httpClient = client;
|
||||
this.jsRuntime = jsRuntime;
|
||||
_resourceMappings = new Lazy<Task<Dictionary<string, string>>>(FetchResourcesAsync);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<MetadataReference>> GetMetadataReferences(string[] wasModules, string[] docModules)
|
||||
{
|
||||
var metadataReferences = new List<MetadataReference>();
|
||||
foreach (var wasModule in wasModules)
|
||||
{
|
||||
var docModule = $"{wasModule}.xml";
|
||||
if(!docModules.Contains(docModule))
|
||||
{
|
||||
docModule = string.Empty;
|
||||
}
|
||||
metadataReferences.Add(await GetMetadataReferenceAsync(wasModule, docModule));
|
||||
}
|
||||
return metadataReferences;
|
||||
}
|
||||
|
||||
private async Task<PortableExecutableReference> GetMetadataReferenceAsync(string wasModule, string docModule)
|
||||
{
|
||||
await using var stream = await httpClient.GetStreamAsync(await ResolveResource($"{wasModule}.wasm"));
|
||||
var peBytes = await WebcilConverterUtil.ConvertFromWebcilAsync(stream);
|
||||
|
||||
using var peStream = new MemoryStream(peBytes);
|
||||
if (string.IsNullOrEmpty(docModule))
|
||||
{
|
||||
return MetadataReference.CreateFromStream(peStream, MetadataReferenceProperties.Assembly);
|
||||
}
|
||||
else
|
||||
{
|
||||
var docBuf = await httpClient.GetByteArrayAsync($"docs/{docModule}");
|
||||
return MetadataReference.CreateFromStream(peStream, MetadataReferenceProperties.Assembly, documentation: XmlDocumentationProvider.CreateFromBytes(docBuf));
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<string> ResolveResource(string logicalName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(logicalName))
|
||||
throw new ArgumentException("Logical name cannot be null or empty.", nameof(logicalName));
|
||||
|
||||
var baseUri = httpClient.BaseAddress?.ToString().TrimEnd('/') ?? "";
|
||||
|
||||
// Strategy 1: Try JavaScript interop to get resource path from Blazor runtime (NET 10+)
|
||||
if (jsRuntime != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var jsPath = await jsRuntime.InvokeAsync<string>("robotnet.blazor.getResourcePath", logicalName);
|
||||
if (!string.IsNullOrEmpty(jsPath) && await TryResourceExists(jsPath))
|
||||
{
|
||||
return jsPath;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// JavaScript function might not be available, continue to other strategies
|
||||
}
|
||||
}
|
||||
|
||||
// Strategy 2: Try direct path (NET 10+ common case)
|
||||
var directPath = $"{baseUri}/_framework/{logicalName}";
|
||||
if (await TryResourceExists(directPath))
|
||||
{
|
||||
return directPath;
|
||||
}
|
||||
|
||||
// Strategy 3: Try to get mapping from boot file (for NET 9 and earlier, or if direct path fails)
|
||||
var resources = await _resourceMappings.Value;
|
||||
if (resources.TryGetValue(logicalName, out var hashedName))
|
||||
{
|
||||
var hashedPath = $"{baseUri}/_framework/{hashedName}";
|
||||
if (await TryResourceExists(hashedPath))
|
||||
{
|
||||
return hashedPath;
|
||||
}
|
||||
}
|
||||
|
||||
throw new FileNotFoundException(
|
||||
$"Resource '{logicalName}' not found. " +
|
||||
$"Tried: JavaScript interop, direct path '{directPath}', " +
|
||||
$"and boot configuration mapping. " +
|
||||
$"In .NET 10, resources may be embedded in dotnet.js. " +
|
||||
$"Please ensure JavaScript function 'robotnet.blazor.getResourcePath' is available.");
|
||||
}
|
||||
|
||||
private async Task<bool> TryResourceExists(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var response = await httpClient.SendAsync(
|
||||
new HttpRequestMessage(HttpMethod.Head, path),
|
||||
HttpCompletionOption.ResponseHeadersRead);
|
||||
return response.IsSuccessStatusCode;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<Dictionary<string, string>> FetchResourcesAsync()
|
||||
{
|
||||
// In NET 10+, boot files are no longer used - resources are accessed directly
|
||||
// This method is kept for backward compatibility with NET 9 and earlier
|
||||
// Return empty dictionary to indicate we should use direct paths
|
||||
var baseUri = httpClient.BaseAddress?.ToString().TrimEnd('/') ?? "";
|
||||
|
||||
// Try blazor.boot.config.json first (some NET 10 preview versions)
|
||||
var bootConfigUrl = $"{baseUri}/_framework/blazor.boot.config.json";
|
||||
try
|
||||
{
|
||||
var bootConfigContent = await httpClient.GetStringAsync(bootConfigUrl);
|
||||
return ParseBootConfigJson(bootConfigContent);
|
||||
}
|
||||
catch (HttpRequestException)
|
||||
{
|
||||
// Fallback to blazor.boot.json (NET 9 and earlier)
|
||||
var bootJsonUrl = $"{baseUri}/_framework/blazor.boot.json";
|
||||
try
|
||||
{
|
||||
var bootJsonContent = await httpClient.GetStringAsync(bootJsonUrl);
|
||||
return ParseBootJson(bootJsonContent);
|
||||
}
|
||||
catch (HttpRequestException)
|
||||
{
|
||||
// NET 10+: No boot file exists, use direct paths
|
||||
// Return empty dictionary - ResolveResource will use direct path
|
||||
return new Dictionary<string, string>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Dictionary<string, string> ParseBootConfigJson(string jsonContent)
|
||||
{
|
||||
using var doc = System.Text.Json.JsonDocument.Parse(jsonContent);
|
||||
var root = doc.RootElement;
|
||||
|
||||
var allResources = new Dictionary<string, string>();
|
||||
|
||||
// NET 10+ uses different structure - check multiple possible locations
|
||||
if (root.TryGetProperty("resources", out var resources))
|
||||
{
|
||||
// Try to get fingerprinting resources (maps logical name -> hashed name)
|
||||
if (resources.TryGetProperty("fingerprinting", out var fingerprinting))
|
||||
{
|
||||
foreach (var prop in fingerprinting.EnumerateObject())
|
||||
{
|
||||
var logicalName = prop.Name;
|
||||
var hashedName = prop.Value.GetString();
|
||||
if (!string.IsNullOrEmpty(hashedName) && !allResources.ContainsKey(logicalName))
|
||||
{
|
||||
allResources[logicalName] = hashedName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Also check for assembly resources directly (maps assembly name -> hashed name)
|
||||
if (resources.TryGetProperty("assembly", out var assembly))
|
||||
{
|
||||
foreach (var prop in assembly.EnumerateObject())
|
||||
{
|
||||
var logicalName = prop.Name;
|
||||
var hashedName = prop.Value.GetString();
|
||||
if (!string.IsNullOrEmpty(hashedName) && !allResources.ContainsKey(logicalName))
|
||||
{
|
||||
allResources[logicalName] = hashedName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for wasmNative resources (for .wasm files)
|
||||
if (resources.TryGetProperty("wasmNative", out var wasmNative))
|
||||
{
|
||||
foreach (var prop in wasmNative.EnumerateObject())
|
||||
{
|
||||
var logicalName = prop.Name;
|
||||
var hashedName = prop.Value.GetString();
|
||||
if (!string.IsNullOrEmpty(hashedName) && !allResources.ContainsKey(logicalName))
|
||||
{
|
||||
allResources[logicalName] = hashedName;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Also check root level for direct mappings (some NET 10 versions might use this)
|
||||
if (root.TryGetProperty("fingerprinting", out var rootFingerprinting))
|
||||
{
|
||||
foreach (var prop in rootFingerprinting.EnumerateObject())
|
||||
{
|
||||
var logicalName = prop.Name;
|
||||
var hashedName = prop.Value.GetString();
|
||||
if (!string.IsNullOrEmpty(hashedName) && !allResources.ContainsKey(logicalName))
|
||||
{
|
||||
allResources[logicalName] = hashedName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return allResources;
|
||||
}
|
||||
|
||||
private Dictionary<string, string> ParseBootJson(string jsonContent)
|
||||
{
|
||||
var bootJson = System.Text.Json.JsonSerializer.Deserialize<BlazorBootJson>(jsonContent);
|
||||
if (bootJson?.Resources?.Fingerprinting == null)
|
||||
{
|
||||
throw new InvalidOperationException("Invalid blazor.boot.json structure.");
|
||||
}
|
||||
|
||||
// Combine all relevant resources into one dictionary for easy lookup
|
||||
var allResources = new Dictionary<string, string>();
|
||||
|
||||
foreach (var resource in bootJson.Resources.Fingerprinting.Where(resource => !allResources.ContainsKey(resource.Value)))
|
||||
{
|
||||
allResources.Add(resource.Value, resource.Key);
|
||||
}
|
||||
|
||||
return allResources;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user