Skip to content

Commit d5c4a4e

Browse files
authored
[browser] Integrate DevServer into WasmAppHost (#88985)
1 parent bee48e8 commit d5c4a4e

21 files changed

+1401
-89
lines changed

src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/Microsoft.NET.Sdk.WebAssembly.Pack.pkgproj

+12
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,20 @@
77

88
<ItemGroup>
99
<ProjectReference Include="$(RepoTasksDir)Microsoft.NET.Sdk.WebAssembly.Pack.Tasks\Microsoft.NET.Sdk.WebAssembly.Pack.Tasks.csproj" />
10+
<ProjectReference Include="$(RepoRoot)src\mono\wasm\host\WasmAppHost.csproj" />
1011
<PackageFile Include="build\*.props;build\*.targets;build\*.web.config" TargetPath="build" />
1112
</ItemGroup>
1213

14+
<Target Name="_PrepareForPack" BeforeTargets="GetPackageFiles">
15+
<ItemGroup>
16+
<PackageFile Include="$(SdkTargetsPath)" TargetPath="Sdk" />
17+
18+
<_WasmAppHostFiles Include="$(WasmAppHostDir)\*" TargetPath="WasmAppHost" />
19+
<PackageFile Include="@(_WasmAppHostFiles)" />
20+
</ItemGroup>
21+
22+
<Error Text="Could not find WasmAppHost files in $(WasmAppHostDir)" Condition="@(_WasmAppHostFiles->Count()) == 0" />
23+
</Target>
24+
1325
<Import Project="$([MSBuild]::GetPathOfFileAbove(Directory.Build.targets))" />
1426
</Project>

src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.targets

+13-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,19 @@ Copyright (c) .NET Foundation. All rights reserved.
1111
-->
1212
<Project ToolsVersion="14.0">
1313

14+
<PropertyGroup>
15+
<_UseBlazorDevServer>$(RunArguments.Contains('blazor-devserver.dll').ToString().ToLower())</_UseBlazorDevServer>
16+
</PropertyGroup>
17+
<PropertyGroup Condition="'$(_WebAssemblyUserRunParameters)' == '' and '$(_UseBlazorDevServer)' == 'false'">
18+
<RunCommand Condition="'$(DOTNET_HOST_PATH)' != '' and Exists($(DOTNET_HOST_PATH))">$(DOTNET_HOST_PATH)</RunCommand>
19+
<RunCommand Condition="'$(RunCommand)' == ''">dotnet</RunCommand>
20+
21+
<WasmAppHostDir>$([MSBuild]::NormalizeDirectory($(MSBuildThisFileDirectory), '..', 'WasmAppHost'))</WasmAppHostDir>
22+
<_RuntimeConfigJsonPath>$([MSBuild]::NormalizePath($(OutputPath), '$(AssemblyName).runtimeconfig.json'))</_RuntimeConfigJsonPath>
23+
<RunArguments>exec &quot;$([MSBuild]::NormalizePath($(WasmAppHostDir), 'WasmAppHost.dll'))&quot; --use-staticwebassets --runtime-config &quot;$(_RuntimeConfigJsonPath)&quot; $(WasmHostArguments)</RunArguments>
24+
<RunWorkingDirectory>$(OutputPath)</RunWorkingDirectory>
25+
</PropertyGroup>
26+
1427
<PropertyGroup>
1528
<EnableDefaultContentItems Condition=" '$(EnableDefaultContentItems)' == '' ">true</EnableDefaultContentItems>
1629

@@ -61,7 +74,6 @@ Copyright (c) .NET Foundation. All rights reserved.
6174

6275
<!-- Turn off parts of the build that do not apply to WASM projects -->
6376
<GenerateDependencyFile>false</GenerateDependencyFile>
64-
<GenerateRuntimeConfigurationFiles>false</GenerateRuntimeConfigurationFiles>
6577
<PreserveCompilationContext>false</PreserveCompilationContext>
6678
<PreserveCompilationReferences>false</PreserveCompilationReferences>
6779
<IsWebConfigTransformDisabled>true</IsWebConfigTransformDisabled>

src/mono/wasm/Wasm.Build.Tests/TestAppScenarios/AppSettingsTests.cs

-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ public async Task LoadAppSettingsBasedOnApplicationEnvironment(string applicatio
3030

3131
var result = await RunSdkStyleApp(new(
3232
Configuration: "Debug",
33-
ForPublish: true,
3433
TestScenario: "AppSettingsTest",
3534
BrowserQueryString: new Dictionary<string, string> { ["applicationEnvironment"] = applicationEnvironment }
3635
));

src/mono/wasm/Wasm.Build.Tests/TestAppScenarios/AppTestBase.cs

+2-3
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,8 @@ protected string GetBinLogFilePath(string suffix = null)
5757

5858
protected async Task<RunResult> RunSdkStyleApp(RunOptions options)
5959
{
60-
string runArgs = $"{s_xharnessRunnerCommand} wasm webserver --app=. --web-server-use-default-files";
61-
string workingDirectory = Path.GetFullPath(Path.Combine(FindBlazorBinFrameworkDir(options.Configuration, forPublish: options.ForPublish), ".."));
60+
string runArgs = $"run -c {options.Configuration}";
61+
string workingDirectory = _projectDir;
6262

6363
using var runCommand = new RunCommand(s_buildEnv, _testOutput)
6464
.WithWorkingDirectory(workingDirectory);
@@ -123,7 +123,6 @@ protected record RunOptions(
123123
string Configuration,
124124
string TestScenario,
125125
Dictionary<string, string> BrowserQueryString = null,
126-
bool ForPublish = false,
127126
Action<IConsoleMessage, IPage> OnConsoleMessage = null,
128127
int? ExpectedExitCode = 0
129128
);

src/mono/wasm/Wasm.Build.Tests/TestAppScenarios/DownloadResourceProgressTests.cs

-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ public async Task DownloadProgressFinishes(bool failAssemblyDownload)
3030

3131
var result = await RunSdkStyleApp(new(
3232
Configuration: "Debug",
33-
ForPublish: true,
3433
TestScenario: "DownloadResourceProgressTest",
3534
BrowserQueryString: new Dictionary<string, string> { ["failAssemblyDownload"] = failAssemblyDownload.ToString().ToLowerInvariant() }
3635
));

src/mono/wasm/Wasm.Build.Tests/TestAppScenarios/LazyLoadingTests.cs

+1-2
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public async Task LoadLazyAssemblyBeforeItIsNeeded()
2626
CopyTestAsset("WasmBasicTestApp", "LazyLoadingTests");
2727
PublishProject("Debug");
2828

29-
var result = await RunSdkStyleApp(new(Configuration: "Debug", ForPublish: true, TestScenario: "LazyLoadingTest"));
29+
var result = await RunSdkStyleApp(new(Configuration: "Debug", TestScenario: "LazyLoadingTest"));
3030
Assert.True(result.TestOutput.Any(m => m.Contains("FirstName")), "The lazy loading test didn't emit expected message with JSON");
3131
}
3232

@@ -38,7 +38,6 @@ public async Task FailOnMissingLazyAssembly()
3838

3939
var result = await RunSdkStyleApp(new(
4040
Configuration: "Debug",
41-
ForPublish: true,
4241
TestScenario: "LazyLoadingTest",
4342
BrowserQueryString: new Dictionary<string, string> { ["loadRequiredAssembly"] = "false" },
4443
ExpectedExitCode: 1

src/mono/wasm/Wasm.Build.Tests/TestAppScenarios/LibraryInitializerTests.cs

+1-2
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public async Task LoadLibraryInitializer()
2929
CopyTestAsset("WasmBasicTestApp", "LibraryInitializerTests_LoadLibraryInitializer");
3030
PublishProject("Debug");
3131

32-
var result = await RunSdkStyleApp(new(Configuration: "Debug", ForPublish: true, TestScenario: "LibraryInitializerTest"));
32+
var result = await RunSdkStyleApp(new(Configuration: "Debug", TestScenario: "LibraryInitializerTest"));
3333
Assert.Collection(
3434
result.TestOutput,
3535
m => Assert.Equal("LIBRARY_INITIALIZER_TEST = 1", m)
@@ -44,7 +44,6 @@ public async Task AbortStartupOnError()
4444

4545
var result = await RunSdkStyleApp(new(
4646
Configuration: "Debug",
47-
ForPublish: true,
4847
TestScenario: "LibraryInitializerTest",
4948
BrowserQueryString: new Dictionary<string, string> { ["throwError"] = "true" },
5049
ExpectedExitCode: 1

src/mono/wasm/Wasm.Build.Tests/TestAppScenarios/SatelliteLoadingTests.cs

+1-2
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,8 @@ public SatelliteLoadingTests(ITestOutputHelper output, SharedBuildPerTestClassFi
2727
public async Task LoadSatelliteAssembly()
2828
{
2929
CopyTestAsset("WasmBasicTestApp", "SatelliteLoadingTests");
30-
PublishProject("Debug");
3130

32-
var result = await RunSdkStyleApp(new(Configuration: "Debug", ForPublish: true, TestScenario: "SatelliteAssembliesTest"));
31+
var result = await RunSdkStyleApp(new(Configuration: "Debug", TestScenario: "SatelliteAssembliesTest"));
3332
Assert.Collection(
3433
result.TestOutput,
3534
m => Assert.Equal("default: hello", m),

src/mono/wasm/host/BrowserArguments.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#nullable enable
55

66
using System.Collections.Generic;
7+
using System.Diagnostics.CodeAnalysis;
78
using System.Text.Json;
89
using Mono.Options;
910

@@ -37,8 +38,8 @@ public void ParseJsonProperties(IDictionary<string, JsonElement>? properties)
3738
ForwardConsoleOutput = forwardConsoleElement.GetBoolean();
3839
}
3940

41+
[SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "Needs to validate instance members")]
4042
public void Validate()
4143
{
42-
CommonConfiguration.CheckPathOrInAppPath(CommonConfig.AppPath, HTMLPath, "html-path");
4344
}
4445
}

src/mono/wasm/host/BrowserHost.cs

+107-27
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,17 @@
44
using System;
55
using System.Collections;
66
using System.Collections.Generic;
7+
using System.Diagnostics.CodeAnalysis;
78
using System.IO;
9+
using System.Linq;
810
using System.Net.WebSockets;
911
using System.Text;
1012
using System.Threading;
1113
using System.Threading.Tasks;
1214
using System.Web;
1315
using Microsoft.AspNetCore.Hosting;
1416
using Microsoft.Extensions.Logging;
17+
using Microsoft.WebAssembly.AppHost.DevServer;
1518
using Microsoft.WebAssembly.Diagnostics;
1619

1720
#nullable enable
@@ -44,7 +47,7 @@ public static async Task<int> InvokeAsync(CommonConfiguration commonArgs,
4447

4548
private async Task RunAsync(ILoggerFactory loggerFactory, CancellationToken token)
4649
{
47-
if (_args.CommonConfig.Debugging)
50+
if (_args.CommonConfig.Debugging && !_args.CommonConfig.UseStaticWebAssets)
4851
{
4952
ProxyOptions options = _args.CommonConfig.ToProxyOptions();
5053
_ = Task.Run(() => DebugProxyHost.RunDebugProxyAsync(options, Array.Empty<string>(), loggerFactory, token), token)
@@ -75,8 +78,7 @@ private async Task RunAsync(ILoggerFactory loggerFactory, CancellationToken toke
7578
? aspnetUrls.Split(';', StringSplitOptions.RemoveEmptyEntries)
7679
: new string[] { $"http://127.0.0.1:{_args.CommonConfig.HostProperties.WebServerPort}", "https://127.0.0.1:0" };
7780

78-
(ServerURLs serverURLs, IWebHost host) = await StartWebServerAsync(_args.CommonConfig.AppPath,
79-
_args.ForwardConsoleOutput ?? false,
81+
(ServerURLs serverURLs, IWebHost host) = await StartWebServerAsync(_args,
8082
urls,
8183
token);
8284

@@ -85,32 +87,104 @@ private async Task RunAsync(ILoggerFactory loggerFactory, CancellationToken toke
8587
foreach (string url in fullUrls)
8688
Console.WriteLine($"App url: {url}");
8789

90+
if (serverURLs.DebugPath != null)
91+
{
92+
Console.WriteLine($"Debug at url: {BuildUrl(serverURLs.Http, serverURLs.DebugPath, string.Empty)}");
93+
94+
if (serverURLs.Https != null)
95+
Console.WriteLine($"Debug at url: {BuildUrl(serverURLs.Https, serverURLs.DebugPath, string.Empty)}");
96+
}
97+
8898
await host.WaitForShutdownAsync(token);
8999
}
90100

91-
private async Task<(ServerURLs, IWebHost)> StartWebServerAsync(string appPath, bool forwardConsole, string[] urls, CancellationToken token)
101+
private async Task<(ServerURLs, IWebHost)> StartWebServerAsync(BrowserArguments args, string[] urls, CancellationToken token)
92102
{
93-
WasmTestMessagesProcessor? logProcessor = null;
94-
if (forwardConsole)
103+
Func<WebSocket, Task>? onConsoleConnected = null;
104+
if (args.ForwardConsoleOutput ?? false)
95105
{
96-
logProcessor = new(_logger);
106+
WasmTestMessagesProcessor logProcessor = new(_logger);
107+
onConsoleConnected = socket => RunConsoleMessagesPump(socket, logProcessor!, token);
97108
}
98109

99-
WebServerOptions options = new
100-
(
101-
OnConsoleConnected: forwardConsole
102-
? socket => RunConsoleMessagesPump(socket, logProcessor!, token)
103-
: null,
104-
ContentRootPath: Path.GetFullPath(appPath),
105-
WebServerUseCors: true,
106-
WebServerUseCrossOriginPolicy: true,
107-
Urls: urls
108-
);
109-
110-
(ServerURLs serverURLs, IWebHost host) = await WebServer.StartAsync(options, _logger, token);
111-
return (serverURLs, host);
110+
// If we are using new browser template, use dev server
111+
if (args.CommonConfig.UseStaticWebAssets)
112+
{
113+
DevServerOptions devServerOptions = CreateDevServerOptions(args, urls, onConsoleConnected);
114+
return await DevServer.DevServer.StartAsync(devServerOptions, _logger, token);
115+
}
116+
117+
// Otherwise for old template, use web server
118+
WebServerOptions webServerOptions = CreateWebServerOptions(urls, args.CommonConfig.AppPath, onConsoleConnected);
119+
return await WebServer.StartAsync(webServerOptions, _logger, token);
112120
}
113121

122+
private static WebServerOptions CreateWebServerOptions(string[] urls, string appPath, Func<WebSocket, Task>? onConsoleConnected) => new
123+
(
124+
OnConsoleConnected: onConsoleConnected,
125+
ContentRootPath: Path.GetFullPath(appPath),
126+
WebServerUseCors: true,
127+
WebServerUseCrossOriginPolicy: true,
128+
Urls: urls
129+
);
130+
131+
private static DevServerOptions CreateDevServerOptions(BrowserArguments args, string[] urls, Func<WebSocket, Task>? onConsoleConnected)
132+
{
133+
const string staticWebAssetsV1Extension = ".StaticWebAssets.xml";
134+
const string staticWebAssetsV2Extension = ".staticwebassets.runtime.json";
135+
136+
DevServerOptions? devServerOptions = null;
137+
138+
string appPath = args.CommonConfig.AppPath;
139+
if (args.CommonConfig.HostProperties.MainAssembly != null)
140+
{
141+
// If we have main assembly name, try to find static web assets manifest by precise name.
142+
143+
var mainAssemblyPath = Path.Combine(appPath, args.CommonConfig.HostProperties.MainAssembly);
144+
var staticWebAssetsPath = Path.ChangeExtension(mainAssemblyPath, staticWebAssetsV2Extension);
145+
if (File.Exists(staticWebAssetsPath))
146+
{
147+
devServerOptions = CreateDevServerOptions(urls, staticWebAssetsPath, onConsoleConnected);
148+
}
149+
else
150+
{
151+
staticWebAssetsPath = Path.ChangeExtension(mainAssemblyPath, staticWebAssetsV1Extension);
152+
if (File.Exists(staticWebAssetsPath))
153+
devServerOptions = CreateDevServerOptions(urls, staticWebAssetsPath, onConsoleConnected);
154+
}
155+
156+
if (devServerOptions == null)
157+
devServerOptions = CreateDevServerOptions(urls, mainAssemblyPath, onConsoleConnected);
158+
}
159+
else
160+
{
161+
// If we don't have main assembly name, try to find static web assets manifest by search in the directory.
162+
163+
var staticWebAssetsPath = FindFirstFileWithExtension(appPath, staticWebAssetsV2Extension)
164+
?? FindFirstFileWithExtension(appPath, staticWebAssetsV1Extension);
165+
166+
if (staticWebAssetsPath != null)
167+
devServerOptions = CreateDevServerOptions(urls, staticWebAssetsPath, onConsoleConnected);
168+
169+
if (devServerOptions == null)
170+
throw new CommandLineException("Please, provide mainAssembly in hostProperties of runtimeconfig");
171+
}
172+
173+
return devServerOptions;
174+
}
175+
176+
private static DevServerOptions CreateDevServerOptions(string[] urls, string staticWebAssetsPath, Func<WebSocket, Task>? onConsoleConnected) => new
177+
(
178+
OnConsoleConnected: onConsoleConnected,
179+
StaticWebAssetsPath: staticWebAssetsPath,
180+
WebServerUseCors: true,
181+
WebServerUseCrossOriginPolicy: true,
182+
Urls: urls
183+
);
184+
185+
private static string? FindFirstFileWithExtension(string directory, string extension)
186+
=> Directory.EnumerateFiles(directory, "*" + extension).First();
187+
114188
private async Task RunConsoleMessagesPump(WebSocket socket, WasmTestMessagesProcessor messagesProcessor, CancellationToken token)
115189
{
116190
byte[] buff = new byte[4000];
@@ -169,7 +243,7 @@ private string[] BuildUrls(ServerURLs serverURLs, IEnumerable<string> passThroug
169243
}
170244

171245
string query = sb.ToString();
172-
string filename = Path.GetFileName(_args.HTMLPath!);
246+
string? filename = _args.HTMLPath != null ? Path.GetFileName(_args.HTMLPath) : null;
173247
string httpUrl = BuildUrl(serverURLs.Http, filename, query);
174248

175249
return string.IsNullOrEmpty(serverURLs.Https)
@@ -179,12 +253,18 @@ private string[] BuildUrls(ServerURLs serverURLs, IEnumerable<string> passThroug
179253
httpUrl,
180254
BuildUrl(serverURLs.Https!, filename, query)
181255
});
256+
}
182257

183-
static string BuildUrl(string baseUrl, string htmlFileName, string query)
184-
=> new UriBuilder(baseUrl)
185-
{
186-
Query = query,
187-
Path = htmlFileName
188-
}.ToString();
258+
private static string BuildUrl(string baseUrl, string? htmlFileName, string query)
259+
{
260+
var uriBuilder = new UriBuilder(baseUrl)
261+
{
262+
Query = query
263+
};
264+
265+
if (htmlFileName != null)
266+
uriBuilder.Path = htmlFileName;
267+
268+
return uriBuilder.ToString();
189269
}
190270
}

0 commit comments

Comments
 (0)