Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ public static IRazorComponentsBuilder AddRazorComponents(this IServiceCollection
services.TryAddScoped<AuthenticationStateProvider, ServerAuthenticationStateProvider>();
services.AddSupplyValueFromQueryProvider();
services.TryAddCascadingValue(sp => sp.GetRequiredService<EndpointHtmlRenderer>().HttpContext);
services.TryAddScoped<WebAssemblySettingsEmitter>();

services.TryAddScoped<ResourceCollectionProvider>();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.Hosting;

namespace Microsoft.AspNetCore.Components.Endpoints;

internal record WebAssemblySettings(string EnvironmentName, Dictionary<string, string> EnvironmentVariables);

internal class WebAssemblySettingsEmitter(IHostEnvironment hostEnvironment)
{
private bool wasEmittedAlready;

private const string dotnetModifiableAssembliesName = "DOTNET_MODIFIABLE_ASSEMBLIES";
private const string aspnetcoreBrowserToolsName = "__ASPNETCORE_BROWSER_TOOLS";

private static readonly string? s_dotnetModifiableAssemblies = GetNonEmptyEnvironmentVariableValue(dotnetModifiableAssembliesName);
private static readonly string? s_aspnetcoreBrowserTools = GetNonEmptyEnvironmentVariableValue(aspnetcoreBrowserToolsName);

private static string? GetNonEmptyEnvironmentVariableValue(string name)
=> Environment.GetEnvironmentVariable(name) is { Length: > 0 } value ? value : null;

public bool TryGetSettingsOnce([NotNullWhen(true)] out WebAssemblySettings? settings)
{
if (wasEmittedAlready)
{
settings = default;
return false;
}

var environmentVariables = new Dictionary<string, string>();

// DOTNET_MODIFIABLE_ASSEMBLIES is used by the runtime to initialize hot-reload specific environment variables and is configured
// by the launching process (dotnet-watch / Visual Studio).
// Always add the header if the environment variable is set, regardless of the kind of environment.
if (s_dotnetModifiableAssemblies != null)
{
environmentVariables[dotnetModifiableAssembliesName] = s_dotnetModifiableAssemblies;
}

// See https://github.com/dotnet/aspnetcore/issues/37357#issuecomment-941237000
// Translate the _ASPNETCORE_BROWSER_TOOLS environment configured by the browser tools agent in to a HTTP response header.
if (s_aspnetcoreBrowserTools != null)
{
environmentVariables[aspnetcoreBrowserToolsName] = s_aspnetcoreBrowserTools;
}

wasEmittedAlready = true;
settings = new (hostEnvironment.EnvironmentName, environmentVariables);
return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,15 @@ private void WriteComponentHtml(int componentId, TextWriter output, bool allowBo
_httpContext.Response.Headers.CacheControl = "no-cache, no-store, max-age=0";
}

if (marker.Type is ComponentMarker.WebAssemblyMarkerType or ComponentMarker.AutoMarkerType)
{
if (_httpContext.RequestServices.GetRequiredService<WebAssemblySettingsEmitter>().TryGetSettingsOnce(out var settings))
{
var settingsJson = JsonSerializer.Serialize(settings, ServerComponentSerializationSettings.JsonSerializationOptions);
output.Write($"<!--Blazor-WebAssembly:{settingsJson}-->");
}
}

var serializedStartRecord = JsonSerializer.Serialize(marker, ServerComponentSerializationSettings.JsonSerializationOptions);
output.Write("<!--Blazor:");
output.Write(serializedStartRecord);
Expand Down
19 changes: 19 additions & 0 deletions src/Components/Endpoints/test/EndpointHtmlRendererTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ public class EndpointHtmlRendererTest
{
private const string MarkerPrefix = "<!--Blazor:";
private const string PrerenderedComponentPattern = "^<!--Blazor:(?<preamble>.*?)-->(?<content>.+?)<!--Blazor:(?<epilogue>.*?)-->$";
private const string WebAssemblyOptionsPattern = "^<!--Blazor-WebAssembly:(.*?)-->";
private const string ComponentPattern = "^<!--Blazor:(.*?)-->$";

private static readonly IDataProtectionProvider _dataprotectorProvider = new EphemeralDataProtectionProvider();
Expand All @@ -57,6 +58,7 @@ public async Task CanRender_ParameterlessComponent_ClientMode()
var result = await renderer.PrerenderComponentAsync(httpContext, typeof(SimpleComponent), new InteractiveWebAssemblyRenderMode(prerender: false), ParameterView.Empty);
await renderer.Dispatcher.InvokeAsync(() => result.WriteTo(writer, HtmlEncoder.Default));
var content = writer.ToString();
content = AssertAndStripWebAssemblyOptions(content);
var match = Regex.Match(content, ComponentPattern);

// Assert
Expand All @@ -80,6 +82,7 @@ public async Task CanPrerender_ParameterlessComponent_ClientMode()
var result = await renderer.PrerenderComponentAsync(httpContext, typeof(SimpleComponent), RenderMode.InteractiveWebAssembly, ParameterView.Empty);
await renderer.Dispatcher.InvokeAsync(() => result.WriteTo(writer, HtmlEncoder.Default));
var content = writer.ToString();
content = AssertAndStripWebAssemblyOptions(content);
var match = Regex.Match(content, PrerenderedComponentPattern, RegexOptions.Multiline);

// Assert
Expand Down Expand Up @@ -123,6 +126,7 @@ public async Task CanRender_ComponentWithParameters_ClientMode()
}));
await renderer.Dispatcher.InvokeAsync(() => result.WriteTo(writer, HtmlEncoder.Default));
var content = writer.ToString();
content = AssertAndStripWebAssemblyOptions(content);
var match = Regex.Match(content, ComponentPattern);

// Assert
Expand Down Expand Up @@ -160,6 +164,7 @@ public async Task CanRender_ComponentWithNullParameters_ClientMode()
}));
await renderer.Dispatcher.InvokeAsync(() => result.WriteTo(writer, HtmlEncoder.Default));
var content = writer.ToString();
content = AssertAndStripWebAssemblyOptions(content);
var match = Regex.Match(content, ComponentPattern);

// Assert
Expand Down Expand Up @@ -195,6 +200,7 @@ public async Task CanPrerender_ComponentWithParameters_ClientMode()
}));
await renderer.Dispatcher.InvokeAsync(() => result.WriteTo(writer, HtmlEncoder.Default));
var content = writer.ToString();
content = AssertAndStripWebAssemblyOptions(content);
var match = Regex.Match(content, PrerenderedComponentPattern, RegexOptions.Multiline);

// Assert
Expand Down Expand Up @@ -244,6 +250,7 @@ public async Task CanPrerender_ComponentWithNullParameters_ClientMode()
}));
await renderer.Dispatcher.InvokeAsync(() => result.WriteTo(writer, HtmlEncoder.Default));
var content = writer.ToString();
content = AssertAndStripWebAssemblyOptions(content);
var match = Regex.Match(content, PrerenderedComponentPattern, RegexOptions.Multiline);

// Assert
Expand Down Expand Up @@ -1063,6 +1070,7 @@ public async Task RenderMode_CanRenderInteractiveComponents()
var lines = content.Replace("\r\n", "\n").Split('\n');
var serverMarkerMatch = Regex.Match(lines[0], PrerenderedComponentPattern);
var serverNonPrerenderedMarkerMatch = Regex.Match(lines[1], ComponentPattern);
lines[2] = AssertAndStripWebAssemblyOptions(lines[2]);
var webAssemblyMarkerMatch = Regex.Match(lines[2], PrerenderedComponentPattern);
var webAssemblyNonPrerenderedMarkerMatch = Regex.Match(lines[3], ComponentPattern);

Expand Down Expand Up @@ -1167,6 +1175,8 @@ public async Task DoesNotEmitNestedRenderModeBoundaries()
var numMarkers = Regex.Matches(content, MarkerPrefix).Count;
Assert.Equal(2, numMarkers); // A start and an end marker

content = AssertAndStripWebAssemblyOptions(content);

var match = Regex.Match(content, PrerenderedComponentPattern, RegexOptions.Singleline);
Assert.True(match.Success);
var preamble = match.Groups["preamble"].Value;
Expand Down Expand Up @@ -1498,6 +1508,14 @@ await renderer.Dispatcher.InvokeAsync(() => renderer.RenderRootComponentAsync(
}
}

private string AssertAndStripWebAssemblyOptions(string content)
{
var wasmOptionsMatch = Regex.Match(content, WebAssemblyOptionsPattern);
Assert.True(wasmOptionsMatch.Success);
content = content.Substring(wasmOptionsMatch.Groups[0].Length);
return content;
}

private class NamedEventHandlerComponent : ComponentBase
{
[Parameter]
Expand Down Expand Up @@ -1681,6 +1699,7 @@ private static ServiceCollection CreateDefaultServiceCollection()
services.AddSingleton<AntiforgeryStateProvider, EndpointAntiforgeryStateProvider>();
services.AddSingleton<ICascadingValueSupplier>(_ => new SupplyParameterFromFormValueProvider(null, ""));
services.AddScoped<ResourceCollectionProvider>();
services.AddSingleton(new WebAssemblySettingsEmitter(new TestEnvironment(Environments.Development)));
return services;
}

Expand Down
18 changes: 12 additions & 6 deletions src/Components/Web.JS/src/Boot.WebAssembly.Common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { SharedMemoryRenderBatch } from './Rendering/RenderBatch/SharedMemoryRen
import { Pointer } from './Platform/Platform';
import { WebAssemblyStartOptions } from './Platform/WebAssemblyStartOptions';
import { addDispatchEventMiddleware } from './Rendering/WebRendererInteropMethods';
import { WebAssemblyComponentDescriptor, discoverWebAssemblyPersistedState } from './Services/ComponentDescriptorDiscovery';
import { WebAssemblyComponentDescriptor, WebAssemblyServerOptions, discoverWebAssemblyPersistedState } from './Services/ComponentDescriptorDiscovery';
import { receiveDotNetDataStream } from './StreamingInterop';
import { WebAssemblyComponentAttacher } from './Platform/WebAssemblyComponentAttacher';
import { MonoConfig } from '@microsoft/dotnet-runtime';
Expand Down Expand Up @@ -68,23 +68,23 @@ export function setWebAssemblyOptions(initializersReady: Promise<Partial<WebAsse
}
}

export function startWebAssembly(components: RootComponentManager<WebAssemblyComponentDescriptor>): Promise<void> {
export function startWebAssembly(components: RootComponentManager<WebAssemblyComponentDescriptor>, options: WebAssemblyServerOptions | undefined): Promise<void> {
if (startPromise !== undefined) {
throw new Error('Blazor WebAssembly has already started.');
}

startPromise = new Promise(startCore.bind(null, components));
startPromise = new Promise(startCore.bind(null, components, options));

return startPromise;
}

async function startCore(components: RootComponentManager<WebAssemblyComponentDescriptor>, resolve, _) {
async function startCore(components: RootComponentManager<WebAssemblyComponentDescriptor>, options: WebAssemblyServerOptions | undefined, resolve, _) {
if (inAuthRedirectIframe()) {
// eslint-disable-next-line @typescript-eslint/no-empty-function
await new Promise(() => { }); // See inAuthRedirectIframe for explanation
}

const platformLoadPromise = loadWebAssemblyPlatformIfNotStarted();
const platformLoadPromise = loadWebAssemblyPlatformIfNotStarted(options);

addDispatchEventMiddleware((browserRendererId, eventHandlerId, continuation) => {
// It's extremely unusual, but an event can be raised while we're in the middle of synchronously applying a
Expand Down Expand Up @@ -206,13 +206,19 @@ export function waitForBootConfigLoaded(): Promise<MonoConfig> {
return bootConfigPromise;
}

export function loadWebAssemblyPlatformIfNotStarted(): Promise<void> {
export function loadWebAssemblyPlatformIfNotStarted(serverOptions: WebAssemblyServerOptions | undefined): Promise<void> {
platformLoadPromise ??= (async () => {
await initializersPromise;
const finalOptions = options ?? {};
if (!finalOptions.environment) {
finalOptions.environment = serverOptions?.environmentName ?? undefined;
}
const existingConfig = options?.configureRuntime;
finalOptions.configureRuntime = (config) => {
existingConfig?.(config);
if (serverOptions?.environmentVariables) {
config.withEnvironmentVariables(serverOptions.environmentVariables);
}
if (waitForRootComponents) {
config.withEnvironmentVariable('__BLAZOR_WEBASSEMBLY_WAIT_FOR_ROOT_COMPONENTS', 'true');
}
Expand Down
6 changes: 4 additions & 2 deletions src/Components/Web.JS/src/Boot.WebAssembly.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Blazor } from './GlobalExports';
import { shouldAutoStart } from './BootCommon';
import { WebAssemblyStartOptions } from './Platform/WebAssemblyStartOptions';
import { setWebAssemblyOptions, startWebAssembly } from './Boot.WebAssembly.Common';
import { WebAssemblyComponentDescriptor, discoverComponents } from './Services/ComponentDescriptorDiscovery';
import { WebAssemblyComponentDescriptor, discoverComponents, discoverWebAssemblyOptions } from './Services/ComponentDescriptorDiscovery';
import { DotNet } from '@microsoft/dotnet-js-interop';
import { InitialRootComponentsList } from './Services/InitialRootComponentsList';
import { JSEventRegistry } from './Services/JSEventRegistry';
Expand All @@ -24,8 +24,10 @@ async function boot(options?: Partial<WebAssemblyStartOptions>): Promise<void> {

JSEventRegistry.create(Blazor);
const webAssemblyComponents = discoverComponents(document, 'webassembly') as WebAssemblyComponentDescriptor[];
const webAssemblyOptions = discoverWebAssemblyOptions(document);

const components = new InitialRootComponentsList(webAssemblyComponents);
await startWebAssembly(components);
await startWebAssembly(components, webAssemblyOptions);
}

Blazor.start = boot;
Expand Down
7 changes: 5 additions & 2 deletions src/Components/Web.JS/src/Rendering/DomMerging/DomSync.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

import { AutoComponentDescriptor, ComponentDescriptor, ServerComponentDescriptor, WebAssemblyComponentDescriptor, canMergeDescriptors, discoverComponents, mergeDescriptors } from '../../Services/ComponentDescriptorDiscovery';
import { AutoComponentDescriptor, ComponentDescriptor, ServerComponentDescriptor, WebAssemblyComponentDescriptor, WebAssemblyServerOptions, canMergeDescriptors, discoverComponents, discoverWebAssemblyOptions, mergeDescriptors } from '../../Services/ComponentDescriptorDiscovery';
import { isInteractiveRootComponentElement } from '../BrowserRenderer';
import { applyAnyDeferredValue } from '../DomSpecialPropertyUtil';
import { LogicalElement, getLogicalChildrenArray, getLogicalNextSibling, getLogicalParent, getLogicalRootDescriptor, insertLogicalChild, insertLogicalChildBefore, isLogicalElement, toLogicalElement, toLogicalRootCommentElement } from '../LogicalElements';
Expand All @@ -13,6 +13,7 @@ let descriptorHandler: DescriptorHandler | null = null;

export interface DescriptorHandler {
registerComponent(descriptor: ComponentDescriptor): void;
setWebAssemblyOptions(options: WebAssemblyServerOptions | undefined): void;
}

export function attachComponentDescriptorHandler(handler: DescriptorHandler) {
Expand All @@ -21,6 +22,8 @@ export function attachComponentDescriptorHandler(handler: DescriptorHandler) {

export function registerAllComponentDescriptors(root: Node) {
const descriptors = upgradeComponentCommentsToLogicalRootComments(root);
const webAssemblyOptions = discoverWebAssemblyOptions(root);
descriptorHandler?.setWebAssemblyOptions(webAssemblyOptions);

for (const descriptor of descriptors) {
descriptorHandler?.registerComponent(descriptor);
Expand Down Expand Up @@ -168,7 +171,7 @@ function treatAsMatch(destination: Node, source: Node) {
}

if (destinationRootDescriptor) {
// Update the existing descriptor with hte new descriptor's data
// Update the existing descriptor with the new descriptor's data
mergeDescriptors(destinationRootDescriptor, sourceRootDescriptor);

const isDestinationInteractive = isInteractiveRootComponentElement(destinationAsLogicalElement);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,16 @@ export function discoverComponents(root: Node, type: 'webassembly' | 'server' |
const blazorServerStateCommentRegularExpression = /^\s*Blazor-Server-Component-State:(?<state>[a-zA-Z0-9+/=]+)$/;
const blazorWebAssemblyStateCommentRegularExpression = /^\s*Blazor-WebAssembly-Component-State:(?<state>[a-zA-Z0-9+/=]+)$/;
const blazorWebInitializerCommentRegularExpression = /^\s*Blazor-Web-Initializers:(?<initializers>[a-zA-Z0-9+/=]+)$/;
const blazorWebAssemblyOptionsCommentRegularExpression = /^\s*Blazor-WebAssembly:[^{]*(?<options>.*)$/;

export function discoverWebAssemblyOptions(root: Node): WebAssemblyServerOptions | undefined {
const optionsJson = discoverBlazorComment(root, blazorWebAssemblyOptionsCommentRegularExpression, 'options');
if (!optionsJson) {
return undefined;
}
const options = JSON.parse(optionsJson);
return options;
}

export function discoverServerPersistedState(node: Node): string | null | undefined {
return discoverBlazorComment(node, blazorServerStateCommentRegularExpression);
Expand Down Expand Up @@ -339,6 +349,11 @@ export type ServerComponentDescriptor = ServerComponentMarker & DescriptorData;
export type WebAssemblyComponentDescriptor = WebAssemblyComponentMarker & DescriptorData;
export type AutoComponentDescriptor = AutoComponentMarker & DescriptorData;

export type WebAssemblyServerOptions = {
environmentName: string,
environmentVariables: { [i: string]: string; }
};

type DescriptorData = {
uniqueId: number;
start: Comment;
Expand Down
12 changes: 9 additions & 3 deletions src/Components/Web.JS/src/Services/WebRootComponentManager.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

import { ComponentDescriptor, ComponentMarker, descriptorToMarker } from './ComponentDescriptorDiscovery';
import { ComponentDescriptor, ComponentMarker, descriptorToMarker, WebAssemblyServerOptions } from './ComponentDescriptorDiscovery';
import { isRendererAttached, registerRendererAttachedListener } from '../Rendering/WebRendererInteropMethods';
import { WebRendererId } from '../Rendering/WebRendererId';
import { DescriptorHandler } from '../Rendering/DomMerging/DomSync';
Expand Down Expand Up @@ -63,6 +63,8 @@ export class WebRootComponentManager implements DescriptorHandler, RootComponent

private _circuitInactivityTimeoutId: any;

private _webAssemblyOptions: WebAssemblyServerOptions | undefined;

// Implements RootComponentManager.
// An empty array becuase all root components managed
// by WebRootComponentManager are added and removed dynamically.
Expand Down Expand Up @@ -94,6 +96,10 @@ export class WebRootComponentManager implements DescriptorHandler, RootComponent
this.rootComponentsMayRequireRefresh();
}

public setWebAssemblyOptions(webAssemblyOptions: WebAssemblyServerOptions | undefined): void {
this._webAssemblyOptions = webAssemblyOptions;
}

public registerComponent(descriptor: ComponentDescriptor) {
if (this._seenDescriptors.has(descriptor)) {
return;
Expand Down Expand Up @@ -132,7 +138,7 @@ export class WebRootComponentManager implements DescriptorHandler, RootComponent

setWaitForRootComponents();

const loadWebAssemblyPromise = loadWebAssemblyPlatformIfNotStarted();
const loadWebAssemblyPromise = loadWebAssemblyPlatformIfNotStarted(this._webAssemblyOptions);
const bootConfig = await waitForBootConfigLoaded();

if (maxParallelDownloadsOverride !== undefined) {
Expand Down Expand Up @@ -182,7 +188,7 @@ export class WebRootComponentManager implements DescriptorHandler, RootComponent
this.startLoadingWebAssemblyIfNotStarted();

if (!hasStartedWebAssembly()) {
await startWebAssembly(this);
await startWebAssembly(this, this._webAssemblyOptions);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@ public static RazorComponentsEndpointConventionBuilder AddInteractiveWebAssembly
var descriptors = StaticAssetsEndpointDataSourceHelper.ResolveStaticAssetDescriptors(endpointBuilder, options.StaticAssetsManifestPath);
if (descriptors != null && descriptors.Count > 0)
{
ComponentWebAssemblyConventions.AddBlazorWebAssemblyConventions(descriptors, environment);
return builder;
}

Expand Down
Loading
Loading