Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.Encodings.Web;
Expand All @@ -12,6 +13,7 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;

namespace Microsoft.AspNetCore.Components.Endpoints;

Expand Down Expand Up @@ -275,6 +277,12 @@ private void WriteComponentHtml(int componentId, TextWriter output, bool allowBo
{
if (_httpContext.RequestServices.GetRequiredService<WebAssemblySettingsEmitter>().TryGetSettingsOnce(out var settings))
{
if (marker.Type is ComponentMarker.WebAssemblyMarkerType)
{
// Preload WebAssembly assets when using WebAssembly (not Auto) mode
AppendWebAssemblyPreloadHeaders();
}

var settingsJson = JsonSerializer.Serialize(settings, ServerComponentSerializationSettings.JsonSerializationOptions);
output.Write($"<!--Blazor-WebAssembly:{settingsJson}-->");
}
Expand Down Expand Up @@ -311,6 +319,75 @@ private void WriteComponentHtml(int componentId, TextWriter output, bool allowBo
}
}

private void AppendWebAssemblyPreloadHeaders()
{
var assets = _httpContext.GetEndpoint()?.Metadata.GetMetadata<ResourceAssetCollection>();
if (assets != null)
{
var headers = new List<(string? Order, string Value)>();
foreach (var asset in assets)
{
if (asset.Properties == null)
{
continue;
}

// Use preloadgroup=webassembly to identify assets that should to be preloaded
string? header = null;
foreach (var property in asset.Properties)
{
if (property.Name.Equals("preloadgroup", StringComparison.OrdinalIgnoreCase) && property.Value.Equals("webassembly", StringComparison.OrdinalIgnoreCase))
{
header = $"<{asset.Url}>";
break;
}
}

if (header == null)
{
continue;
}

string? order = null;
foreach (var property in asset.Properties)
{
if (property.Name.Equals("preloadrel", StringComparison.OrdinalIgnoreCase))
{
header = String.Concat(header, "; rel=", property.Value);
}
else if (property.Name.Equals("preloadas", StringComparison.OrdinalIgnoreCase))
{
header = String.Concat(header, "; as=", property.Value);
}
else if (property.Name.Equals("preloadpriority", StringComparison.OrdinalIgnoreCase))
{
header = String.Concat(header, "; fetchpriority=", property.Value);
}
else if (property.Name.Equals("preloadcrossorigin", StringComparison.OrdinalIgnoreCase))
{
header = String.Concat(header, "; crossorigin=", property.Value);
}
else if (property.Name.Equals("integrity", StringComparison.OrdinalIgnoreCase))
{
header = String.Concat(header, "; integrity=\"", property.Value, "\"");
}
else if (property.Name.Equals("preloadorder", StringComparison.OrdinalIgnoreCase))
{
order = property.Value;
}
}

if (header != null)
{
headers.Add((order, header));
}
}

headers.Sort((a, b) => string.Compare(a.Order, b.Order, StringComparison.InvariantCulture));
_httpContext.Response.Headers.Link = StringValues.Concat(_httpContext.Response.Headers.Link, headers.Select(h => h.Value).ToArray());
}
}

private static bool IsProgressivelyEnhancedNavigation(HttpRequest request)
{
// For enhanced nav, the Blazor JS code controls the "accept" header precisely, so we can be very specific about the format
Expand Down
71 changes: 70 additions & 1 deletion src/Components/Endpoints/test/EndpointHtmlRendererTest.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Net.Http;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.RegularExpressions;
Expand Down Expand Up @@ -71,6 +70,76 @@ public async Task CanRender_ParameterlessComponent_ClientMode()
Assert.Empty(httpContext.Items);
}

[Fact]
public async Task CanPreload_WebAssembly_ResourceAssets()
{
// Arrange
var httpContext = GetHttpContext();
var writer = new StringWriter();

httpContext.SetEndpoint(
new Endpoint(
ctx => Task.CompletedTask,
new EndpointMetadataCollection([
new ResourceAssetCollection([
new ResourceAsset("second.js", [
new ResourceAssetProperty("preloadrel", "preload"),
new ResourceAssetProperty("preloadas", "script"),
new ResourceAssetProperty("preloadpriority", "high"),
new ResourceAssetProperty("preloadcrossorigin", "anonymous"),
new ResourceAssetProperty("integrity", "abcd"),
new ResourceAssetProperty("preloadorder", "2"),
new ResourceAssetProperty("preloadgroup", "webassembly")
]),
new ResourceAsset("first.js", [
new ResourceAssetProperty("preloadrel", "preload"),
new ResourceAssetProperty("preloadas", "script"),
new ResourceAssetProperty("preloadpriority", "high"),
new ResourceAssetProperty("preloadcrossorigin", "anonymous"),
new ResourceAssetProperty("integrity", "abcd"),
new ResourceAssetProperty("preloadorder", "1"),
new ResourceAssetProperty("preloadgroup", "webassembly")
]),
new ResourceAsset("preload-nowebassembly.js", [
new ResourceAssetProperty("preloadrel", "preload"),
new ResourceAssetProperty("preloadas", "script"),
new ResourceAssetProperty("preloadpriority", "high"),
new ResourceAssetProperty("preloadcrossorigin", "anonymous"),
new ResourceAssetProperty("integrity", "abcd"),
new ResourceAssetProperty("preloadorder", "1"),
new ResourceAssetProperty("preloadgroup", "abcd")
]),
new ResourceAsset("nopreload.js", [
new ResourceAssetProperty("integrity", "abcd")
])
])
]),
"TestEndpoint"
)
);

// Act
var result = await renderer.PrerenderComponentAsync(httpContext, typeof(SimpleComponent), new InteractiveWebAssemblyRenderMode(prerender: false), ParameterView.Empty);
await renderer.Dispatcher.InvokeAsync(() => result.WriteTo(writer, HtmlEncoder.Default));

// Assert
Assert.Equal(2, httpContext.Response.Headers.Link.Count);

var firstPreloadLink = httpContext.Response.Headers.Link[0];
Assert.Contains("<first.js>", firstPreloadLink);
Assert.Contains("rel=preload", firstPreloadLink);
Assert.Contains("as=script", firstPreloadLink);
Assert.Contains("fetchpriority=high", firstPreloadLink);
Assert.Contains("integrity=\"abcd\"", firstPreloadLink);

var secondPreloadLink = httpContext.Response.Headers.Link[1];
Assert.Contains("<second.js>", secondPreloadLink);
Assert.Contains("rel=preload", secondPreloadLink);
Assert.Contains("as=script", secondPreloadLink);
Assert.Contains("fetchpriority=high", secondPreloadLink);
Assert.Contains("integrity=\"abcd\"", secondPreloadLink);
}

[Fact]
public async Task CanPrerender_ParameterlessComponent_ClientMode()
{
Expand Down
83 changes: 80 additions & 3 deletions src/Shared/Components/ResourceCollectionResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,21 @@ public ResourceAssetCollection ResolveResourceCollection()
#if !MVC_VIEWFEATURES
string? label = null;
string? integrity = null;
string? preloadRel = null;
string? preloadAs = null;
string? preloadPriority = null;
string? preloadCrossorigin = null;
string? preloadOrder = null;
string? preloadGroup = null;
#else
string label = null;
string integrity = null;
string preloadRel = null;
string preloadAs = null;
string preloadPriority = null;
string preloadCrossorigin = null;
string preloadOrder = null;
string preloadGroup = null;
#endif

// If there's a selector this means that this is an alternative representation for a resource, so skip it.
Expand All @@ -59,15 +71,44 @@ public ResourceAssetCollection ResolveResourceCollection()
label = property.Value;
foundProperties++;
}

else if (property.Name.Equals("integrity", StringComparison.OrdinalIgnoreCase))
{
integrity = property.Value;
foundProperties++;
}
else if (property.Name.Equals("preloadrel", StringComparison.OrdinalIgnoreCase))
{
preloadRel = property.Value;
foundProperties++;
}
else if (property.Name.Equals("preloadas", StringComparison.OrdinalIgnoreCase))
{
preloadAs = property.Value;
foundProperties++;
}
else if (property.Name.Equals("preloadpriority", StringComparison.OrdinalIgnoreCase))
{
preloadPriority = property.Value;
foundProperties++;
}
else if (property.Name.Equals("preloadcrossorigin", StringComparison.OrdinalIgnoreCase))
{
preloadCrossorigin = property.Value;
foundProperties++;
}
else if (property.Name.Equals("preloadorder", StringComparison.OrdinalIgnoreCase))
{
preloadOrder = property.Value;
foundProperties++;
}
else if (property.Name.Equals("preloadgroup", StringComparison.OrdinalIgnoreCase))
{
preloadGroup = property.Value;
foundProperties++;
}
}

AddResource(resources, descriptor, label, integrity, foundProperties);
AddResource(resources, descriptor, label, integrity, preloadRel, preloadAs, preloadPriority, preloadCrossorigin, preloadOrder, preloadGroup, foundProperties);
}
}

Expand Down Expand Up @@ -97,11 +138,23 @@ private static void AddResource(
#if !MVC_VIEWFEATURES
string? label,
string? integrity,
string? preloadRel,
string? preloadAs,
string? preloadPriority,
string? preloadCrossorigin,
string? preloadOrder,
string? preloadGroup,
#else
string label,
string integrity,
string preloadRel,
string preloadAs,
string preloadPriority,
string preloadCrossorigin,
string preloadOrder,
string preloadGroup,
#endif
int foundProperties)
int foundProperties)
{
if (label != null || integrity != null)
{
Expand All @@ -115,6 +168,30 @@ private static void AddResource(
{
properties[index++] = new ResourceAssetProperty("integrity", integrity);
}
if (preloadRel != null)
{
properties[index++] = new ResourceAssetProperty("preloadrel", preloadRel);
}
if (preloadAs != null)
{
properties[index++] = new ResourceAssetProperty("preloadas", preloadAs);
}
if (preloadPriority != null)
{
properties[index++] = new ResourceAssetProperty("preloadpriority", preloadPriority);
}
if (preloadCrossorigin != null)
{
properties[index++] = new ResourceAssetProperty("preloadcrossorigin", preloadCrossorigin);
}
if (preloadOrder != null)
{
properties[index++] = new ResourceAssetProperty("preloadorder", preloadOrder);
}
if (preloadGroup != null)
{
properties[index++] = new ResourceAssetProperty("preloadgroup", preloadGroup);
}

resources.Add(new ResourceAsset(descriptor.Route, properties));
}
Expand Down
Loading