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>> _resourceMappings; public ScriptResourceResolver(HttpClient client, IJSRuntime? jsRuntime = null) { httpClient = client; this.jsRuntime = jsRuntime; _resourceMappings = new Lazy>>(FetchResourcesAsync); } public async Task> GetMetadataReferences(string[] wasModules, string[] docModules) { var metadataReferences = new List(); 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 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 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("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 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> 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(); } } } private Dictionary ParseBootConfigJson(string jsonContent) { using var doc = System.Text.Json.JsonDocument.Parse(jsonContent); var root = doc.RootElement; var allResources = new Dictionary(); // 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 ParseBootJson(string jsonContent) { var bootJson = System.Text.Json.JsonSerializer.Deserialize(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(); foreach (var resource in bootJson.Resources.Fingerprinting.Where(resource => !allResources.ContainsKey(resource.Value))) { allResources.Add(resource.Value, resource.Key); } return allResources; } }