Merge commit 'bbafecc0535e1de3264845e51ea8b3d18eb3ca61' into prkrishn/merge-blazor-wasm

This commit is contained in:
Pranav K 2020-05-15 10:12:32 -07:00
commit 8966415b6b
No known key found for this signature in database
GPG Key ID: F748807460A27E91
261 changed files with 6890 additions and 3868 deletions

View File

@ -0,0 +1,59 @@
# Uses Scheduled Triggers, which aren't supported in YAML yet.
# https://docs.microsoft.com/en-us/azure/devops/pipelines/build/triggers?view=vsts&tabs=yaml#scheduled
# Daily Tests for Blazor
# These use Sauce Labs resources, hence they run daily rather than per-commit.
# We just need one Windows machine because all it does is trigger SauceLabs.
variables:
SAUCE_CONNECT_DOWNLOAD_ON_INSTALL: true
E2ETESTS_SauceTest: true
E2ETESTS_Sauce__TunnelIdentifier: 'blazor-e2e-sc-proxy-tunnel'
E2ETESTS_Sauce__HostName: 'sauce.local'
jobs:
- template: jobs/default-build.yml
parameters:
buildDirectory: src/Components
isTestingJob: true
agentOs: Windows
jobName: BlazorDailyTests
jobDisplayName: "Blazor Daily Tests"
afterBuild:
# macOS/Safari
- script: 'dotnet test --filter "StandaloneAppTest"'
workingDirectory: 'src/Components/test/E2ETest'
displayName: 'Run Blazor tests - macOS/Safari'
condition: succeededOrFailed()
env:
# Secrets need to be explicitly mapped to env variables.
E2ETESTS_Sauce__Username: '$(asplab-sauce-labs-username)'
E2ETESTS_Sauce__AccessKey: '$(asplab-sauce-labs-access-key)'
# Set platform/browser configuration.
E2ETESTS_Sauce__TestName: 'Blazor Daily Tests - macOS/Safari'
E2ETESTS_Sauce__PlatformName: 'macOS 10.14'
E2ETESTS_Sauce__BrowserName: 'Safari'
# Need to explicitly set version here because some older versions don't support timeouts in Safari.
E2ETESTS_Sauce__SeleniumVersion: '3.4.0'
# Android/Chrome
- script: 'dotnet test --filter "StandaloneAppTest"'
workingDirectory: 'src/Components/test/E2ETest'
displayName: 'Run Blazor tests - Android/Chrome'
condition: succeededOrFailed()
env:
# Secrets need to be explicitly mapped to env variables.
E2ETESTS_Sauce__Username: '$(asplab-sauce-labs-username)'
E2ETESTS_Sauce__AccessKey: '$(asplab-sauce-labs-access-key)'
# Set platform/browser configuration.
E2ETESTS_Sauce__TestName: 'Blazor Daily Tests - Android/Chrome'
E2ETESTS_Sauce__PlatformName: 'Android'
E2ETESTS_Sauce__PlatformVersion: '10.0'
E2ETESTS_Sauce__BrowserName: 'Chrome'
E2ETESTS_Sauce__DeviceName: 'Android GoogleAPI Emulator'
E2ETESTS_Sauce__DeviceOrientation: 'portrait'
E2ETESTS_Sauce__AppiumVersion: '1.9.1'
artifacts:
- name: Windows_Logs
path: ../../artifacts/log/
publishOnError: true

View File

@ -7,9 +7,9 @@ trigger:
batch: true
branches:
include:
- blazor-wasm
- master
- release/*
- internal/release/3.*
# Run PR validation on all branches
pr:
@ -80,11 +80,18 @@ variables:
value: test
- name: _PublishArgs
value: ''
<<<<<<< HEAD
=======
# used for post-build phases, internal builds only
- ${{ if and(ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}:
- group: DotNet-AspNet-SDLValidation-Params
>>>>>>> bbafecc0535e1de3264845e51ea8b3d18eb3ca61
stages:
- stage: build
displayName: Build
jobs:
<<<<<<< HEAD
# Code check
- ${{ if or(eq(variables['System.TeamProject'], 'public'), in(variables['Build.Reason'], 'PullRequest')) }}:
- template: jobs/default-build.yml
@ -109,6 +116,8 @@ stages:
publishOnError: true
includeForks: true
=======
>>>>>>> bbafecc0535e1de3264845e51ea8b3d18eb3ca61
# Build Windows (x64/x86)
- template: jobs/default-build.yml
parameters:
@ -142,12 +151,13 @@ stages:
-arch x64
-pack
-all
-buildNative
-NoBuildNative
/bl:artifacts/log/build.x64.binlog
$(_BuildArgs)
$(_InternalRuntimeDownloadArgs)
displayName: Build x64
<<<<<<< HEAD
# Build the x86 shared framework
# TODO: make it possible to build for one Windows architecture at a time
# This is going to actually build x86 native assets. See https://github.com/aspnet/AspNetCore/issues/7196
@ -173,6 +183,8 @@ stages:
$(_InternalRuntimeDownloadArgs)
displayName: Build SiteExtension
=======
>>>>>>> bbafecc0535e1de3264845e51ea8b3d18eb3ca61
# This runs code-signing on all packages, zips, and jar files as defined in build/CodeSign.targets. If https://github.com/dotnet/arcade/issues/1957 is resolved,
# consider running code-signing inline with the other previous steps.
# Sign check is disabled because it is run in a separate step below, after installers are built.
@ -200,16 +212,6 @@ stages:
/p:PublishInstallerBaseVersion=true
displayName: Build Installers
# A few files must also go to the VS package feed.
- ${{ if and(ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}:
- task: NuGetCommand@2
displayName: Push Visual Studio packages
inputs:
command: push
packagesToPush: 'artifacts/packages/**/VS.Redist.Common.AspNetCore.*.nupkg'
nuGetFeedType: external
publishFeedCredentials: 'DevDiv - VS package feed'
artifacts:
- name: Windows_Logs
path: artifacts/log/
@ -218,6 +220,7 @@ stages:
- name: Windows_Packages
path: artifacts/packages/
<<<<<<< HEAD
# Build Windows ARM
- template: jobs/default-build.yml
parameters:
@ -512,6 +515,8 @@ stages:
parameters:
inputName: Linux_musl_arm64
=======
>>>>>>> bbafecc0535e1de3264845e51ea8b3d18eb3ca61
# Test jobs
- template: jobs/default-build.yml
parameters:
@ -656,6 +661,7 @@ stages:
publishOnError: true
includeForks: true
<<<<<<< HEAD
# Source build
- job: Source_Build
displayName: 'Test: Linux Source Build'
@ -714,23 +720,17 @@ stages:
artifactType: Container
parallel: true
=======
>>>>>>> bbafecc0535e1de3264845e51ea8b3d18eb3ca61
# Publish to the BAR
- ${{ if and(ne(variables['System.TeamProject'], 'public'), notin(variables['Build.Reason'], 'PullRequest')) }}:
- template: /eng/common/templates/job/publish-build-assets.yml
parameters:
dependsOn:
- Windows_build
- Windows_arm_build
- CodeSign_Xplat_MacOS_x64
- CodeSign_Xplat_Linux_x64
- CodeSign_Xplat_Linux_arm
- CodeSign_Xplat_Linux_arm64
- CodeSign_Xplat_Linux_musl_x64
- CodeSign_Xplat_Linux_musl_arm64
# In addition to the dependencies above, ensure the build was successful overall.
- Linux_Test
- MacOS_Test
- Source_Build
- Windows_Templates_Test
- Windows_Test
pool:

12
.config/dotnet-tools.json Normal file
View File

@ -0,0 +1,12 @@
{
"version": 1,
"isRoot": true,
"tools": {
"dotnet-serve": {
"version": "1.5.0",
"commands": [
"dotnet-serve"
]
}
}
}

View File

@ -3,6 +3,7 @@
<packageSources>
<clear />
<!--Begin: Package sources managed by Dependency Flow automation. Do not edit the sources below.-->
<<<<<<< HEAD
<add key="darc-pub-dotnet-corefx-8a3ffed" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/darc-pub-dotnet-corefx-8a3ffed5/nuget/v3/index.json" />
<!--End: Package sources managed by Dependency Flow automation. Do not edit the sources above.-->
<add key="dotnet-core" value="https://dotnetfeed.blob.core.windows.net/dotnet-core/index.json" />
@ -10,5 +11,24 @@
<add key="dotnet3.1" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet3.1/nuget/v3/index.json" />
<add key="dotnet3.1-transport" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet3.1-transport/nuget/v3/index.json" />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
=======
<add key="darc-pub-dotnet-core-setup-65f04fb" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/darc-pub-dotnet-core-setup-65f04fb6/nuget/v3/index.json" />
<add key="darc-pub-dotnet-corefx-0f7f38c" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/darc-pub-dotnet-corefx-0f7f38c4/nuget/v3/index.json" />
<!--End: Package sources managed by Dependency Flow automation. Do not edit the sources above.-->
<add key="dotnet-core" value="https://dotnetfeed.blob.core.windows.net/dotnet-core/index.json" />
<add key="dotnet-tools" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-tools/nuget/v3/index.json" />
<add key="aspnet-blazor" value="https://dotnetfeed.blob.core.windows.net/aspnet-blazor/index.json" />
<add key="aspnet-extensions" value="https://dotnetfeed.blob.core.windows.net/aspnet-extensions/index.json" />
<add key="aspnet-entityframeworkcore" value="https://dotnetfeed.blob.core.windows.net/aspnet-entityframeworkcore/index.json" />
<add key="aspnet-aspnetcore-tooling" value="https://dotnetfeed.blob.core.windows.net/aspnet-aspnetcore-tooling/index.json" />
<add key="aspnet-stable" value="https://dotnetfeed.blob.core.windows.net/dotnet-core-3-1-rtm-014727/index.json" />
<add key="grpc-nuget-dev" value="https://grpc.jfrog.io/grpc/api/nuget/v3/grpc-nuget-dev" />
<add key="roslyn" value="https://dotnet.myget.org/F/roslyn/api/v3/index.json" />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
<add key="aspnetcore-dev" value="https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json" />
<add key="aspnetcore-tools" value="https://dotnet.myget.org/F/aspnetcore-tools/api/v3/index.json" />
<add key="roslyn-tools" value="https://dotnet.myget.org/F/roslyn-tools/api/v3/index.json" />
<add key="blazor-wasm" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet3.1-blazor/nuget/v3/index.json" />
>>>>>>> bbafecc0535e1de3264845e51ea8b3d18eb3ca61
</packageSources>
</configuration>

View File

@ -34,7 +34,8 @@
$(RepoRoot)src\Installers\**\*.*proj;
$(RepoRoot)src\SignalR\clients\ts\**\node_modules\**\*.*proj;
$(RepoRoot)src\Components\Web.JS\node_modules\**\*.*proj;
$(RepoRoot)src\Components\Blazor\Templates\src\content\**\*.*proj;
$(RepoRoot)src\Components\Blazor\Build\testassets\**\*.*proj;
$(RepoRoot)src\ProjectTemplates\BlazorWasm.ProjectTemplates\content\**\*.csproj;
$(RepoRoot)src\ProjectTemplates\Web.ProjectTemplates\content\**\*.csproj;
$(RepoRoot)src\ProjectTemplates\Web.ProjectTemplates\content\**\*.fsproj;
$(RepoRoot)src\ProjectTemplates\Web.Spa.ProjectTemplates\content\**\*.csproj;
@ -49,6 +50,11 @@
" />
</ItemGroup>
<PropertyGroup>
<!-- For the Blazor WASM branch, only build a subset of projects -->
<ProjectToBuild>$(RepoRoot)src\Components\**\*.csproj;$(RepoRoot)src\ProjectTemplates\**\*.csproj</ProjectToBuild>
</PropertyGroup>
<Choose>
<!-- Project selection can be overridden on the command line by passing in -projects -->
<When Condition="'$(ProjectToBuild)' != ''">

View File

@ -257,7 +257,7 @@ function InitializeVisualStudioMSBuild([bool]$install, [object]$vsRequirements =
if ($msbuildCmd -ne $null) {
# Workaround for https://github.com/dotnet/roslyn/issues/35793
# Due to this issue $msbuildCmd.Version returns 0.0.0.0 for msbuild.exe 16.2+
$msbuildVersion = [Version]::new((Get-Item $msbuildCmd.Path).VersionInfo.ProductVersion.Split(@('-', '+'))[0])
$msbuildVersion = [Version]::new((Get-Item $msbuildCmd.Path).VersionInfo.ProductVersion.Split([char[]]@('-', '+'))[0])
if ($msbuildVersion -ge $vsMinVersion) {
return $global:_MSBuildExe = $msbuildCmd.Path

View File

@ -170,10 +170,11 @@ try {
& $PSScriptRoot\GenerateReferenceAssemblies.ps1 -ci:$ci
}
Write-Host "Re-generating package baselines"
Invoke-Block {
& dotnet run -p "$repoRoot/eng/tools/BaselineGenerator/"
}
# Temporarily disable package baseline generation while we stage for publishing
# Write-Host "Re-generating package baselines"
# Invoke-Block {
# & dotnet run -p "$repoRoot/eng/tools/BaselineGenerator/"
# }
Write-Host "Run git diff to check for pending changes"

View File

@ -1,9 +1,16 @@
{
"sdk": {
<<<<<<< HEAD
"version": "3.1.103"
},
"tools": {
"dotnet": "3.1.103",
=======
"version": "3.1.100"
},
"tools": {
"dotnet": "3.1.100",
>>>>>>> bbafecc0535e1de3264845e51ea8b3d18eb3ca61
"runtimes": {
"dotnet/x64": [
"$(MicrosoftNETCoreAppInternalPackageVersion)"
@ -25,7 +32,12 @@
},
"msbuild-sdks": {
"Yarn.MSBuild": "1.15.2",
<<<<<<< HEAD
"Microsoft.DotNet.Arcade.Sdk": "1.0.0-beta.20213.4",
"Microsoft.DotNet.Helix.Sdk": "2.0.0-beta.20213.4"
=======
"Microsoft.DotNet.Arcade.Sdk": "1.0.0-beta.19577.5",
"Microsoft.DotNet.Helix.Sdk": "2.0.0-beta.19577.5"
>>>>>>> bbafecc0535e1de3264845e51ea8b3d18eb3ca61
}
}

View File

@ -0,0 +1,8 @@
<Project>
<PropertyGroup>
<!-- Override version labels -->
<VersionPrefix>3.2.0</VersionPrefix>
<PreReleaseVersionLabel>preview1</PreReleaseVersionLabel>
<DotNetFinalVersionKind />
</PropertyGroup>
</Project>

View File

@ -0,0 +1,69 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNetCore.Blazor
{
[System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)]
public static partial class JSInteropMethods
{
[Microsoft.JSInterop.JSInvokableAttribute("NotifyLocationChanged")]
public static void NotifyLocationChanged(string uri, bool isInterceptedLink) { }
}
}
namespace Microsoft.AspNetCore.Blazor.Hosting
{
[System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)]
public readonly partial struct RootComponentMapping
{
private readonly object _dummy;
public RootComponentMapping(System.Type componentType, string selector) { throw null; }
public System.Type ComponentType { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
public string Selector { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
}
public partial class RootComponentMappingCollection : System.Collections.ObjectModel.Collection<Microsoft.AspNetCore.Blazor.Hosting.RootComponentMapping>
{
public RootComponentMappingCollection() { }
public void Add(System.Type componentType, string selector) { }
public void AddRange(System.Collections.Generic.IEnumerable<Microsoft.AspNetCore.Blazor.Hosting.RootComponentMapping> items) { }
public void Add<TComponent>(string selector) where TComponent : Microsoft.AspNetCore.Components.IComponent { }
}
public sealed partial class WebAssemblyHost : System.IAsyncDisposable
{
internal WebAssemblyHost() { }
public Microsoft.Extensions.Configuration.IConfiguration Configuration { get { throw null; } }
public System.IServiceProvider Services { get { throw null; } }
[System.Diagnostics.DebuggerStepThroughAttribute]
public System.Threading.Tasks.ValueTask DisposeAsync() { throw null; }
public System.Threading.Tasks.Task RunAsync() { throw null; }
}
public sealed partial class WebAssemblyHostBuilder
{
internal WebAssemblyHostBuilder() { }
public Microsoft.Extensions.Configuration.IConfigurationBuilder Configuration { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
public Microsoft.AspNetCore.Blazor.Hosting.RootComponentMappingCollection RootComponents { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
public Microsoft.Extensions.DependencyInjection.IServiceCollection Services { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
public Microsoft.AspNetCore.Blazor.Hosting.WebAssemblyHost Build() { throw null; }
public static Microsoft.AspNetCore.Blazor.Hosting.WebAssemblyHostBuilder CreateDefault(string[] args = null) { throw null; }
}
}
namespace Microsoft.AspNetCore.Blazor.Http
{
public enum FetchCredentialsOption
{
Omit = 0,
SameOrigin = 1,
Include = 2,
}
public static partial class WebAssemblyHttpMessageHandlerOptions
{
public static Microsoft.AspNetCore.Blazor.Http.FetchCredentialsOption DefaultCredentials { get { throw null; } set { } }
}
}
namespace Microsoft.AspNetCore.Blazor.Rendering
{
public static partial class WebAssemblyEventDispatcher
{
[Microsoft.JSInterop.JSInvokableAttribute("DispatchEvent")]
public static System.Threading.Tasks.Task DispatchEvent(Microsoft.AspNetCore.Components.RenderTree.WebEventDescriptor eventDescriptor, string eventArgsJson) { throw null; }
}
}

View File

@ -1,24 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNetCore.Components.Builder
{
/// <summary>
/// Provides extension methods for <see cref="IComponentsApplicationBuilder"/>.
/// </summary>
public static class ComponentsApplicationBuilderExtensions
{
/// <summary>
/// Associates the component type with the application,
/// causing it to be displayed in the specified DOM element.
/// </summary>
/// <param name="app">The <see cref="IComponentsApplicationBuilder"/>.</param>
/// <typeparam name="TComponent">The type of the component.</typeparam>
/// <param name="domElementSelector">A CSS selector that uniquely identifies a DOM element.</param>
public static void AddComponent<TComponent>(this IComponentsApplicationBuilder app, string domElementSelector)
where TComponent : IComponent
{
app.AddComponent(typeof(TComponent), domElementSelector);
}
}
}

View File

@ -1,26 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
namespace Microsoft.AspNetCore.Components.Builder
{
/// <summary>
/// A builder for adding components to an application.
/// </summary>
public interface IComponentsApplicationBuilder
{
/// <summary>
/// Gets the application services.
/// </summary>
IServiceProvider Services { get; }
/// <summary>
/// Associates the <see cref="IComponent"/> with the application,
/// causing it to be displayed in the specified DOM element.
/// </summary>
/// <param name="componentType">The type of the component.</param>
/// <param name="domElementSelector">A CSS selector that uniquely identifies a DOM element.</param>
void AddComponent(Type componentType, string domElementSelector);
}
}

View File

@ -1,20 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNetCore.Blazor.Hosting
{
/// <summary>
/// Used to create an instance of Blazor host builder for a Browser application.
/// </summary>
public static class BlazorWebAssemblyHost
{
/// <summary>
/// Creates an instance of <see cref="IWebAssemblyHostBuilder"/>.
/// </summary>
/// <returns>The <see cref="IWebAssemblyHostBuilder"/>.</returns>
public static IWebAssemblyHostBuilder CreateDefaultBuilder()
{
return new WebAssemblyHostBuilder();
}
}
}

View File

@ -1,117 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Runtime.ExceptionServices;
using Microsoft.AspNetCore.Components.Builder;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.AspNetCore.Blazor.Hosting
{
// Keeping this simple for now to focus on predictable and reasonable behaviors.
// Startup in WebHost supports lots of things we don't yet support, and some we
// may never support.
//
// Possible additions:
// - environments
// - case-insensitivity (makes sense with environments)
//
// Likely never:
// - statics
// - DI into constructor
internal class ConventionBasedStartup : IBlazorStartup
{
public ConventionBasedStartup(object instance)
{
Instance = instance ?? throw new ArgumentNullException(nameof(instance));
}
public object Instance { get; }
public void Configure(IComponentsApplicationBuilder app, IServiceProvider services)
{
try
{
var method = GetConfigureMethod();
Debug.Assert(method != null);
var parameters = method.GetParameters();
var arguments = new object[parameters.Length];
for (var i = 0; i < parameters.Length; i++)
{
var parameter = parameters[i];
arguments[i] = parameter.ParameterType == typeof(IComponentsApplicationBuilder)
? app
: services.GetRequiredService(parameter.ParameterType);
}
method.Invoke(Instance, arguments);
}
catch (Exception ex)
{
if (ex is TargetInvocationException)
{
ExceptionDispatchInfo.Capture(ex.InnerException).Throw();
}
throw;
}
}
internal MethodInfo GetConfigureMethod()
{
var methods = Instance.GetType()
.GetMethods(BindingFlags.Instance | BindingFlags.Public)
.Where(m => string.Equals(m.Name, "Configure", StringComparison.Ordinal))
.ToArray();
if (methods.Length == 1)
{
return methods[0];
}
else if (methods.Length == 0)
{
throw new InvalidOperationException("The startup class must define a 'Configure' method.");
}
else
{
throw new InvalidOperationException("Overloading the 'Configure' method is not supported.");
}
}
public void ConfigureServices(IServiceCollection services)
{
try
{
var method = GetConfigureServicesMethod();
if (method != null)
{
method.Invoke(Instance, new object[] { services });
}
}
catch (Exception ex)
{
if (ex is TargetInvocationException)
{
ExceptionDispatchInfo.Capture(ex.InnerException).Throw();
}
throw;
}
}
internal MethodInfo GetConfigureServicesMethod()
{
return Instance.GetType()
.GetMethod(
"ConfigureServices",
BindingFlags.Public | BindingFlags.Instance,
null,
new Type[] { typeof(IServiceCollection), },
Array.Empty<ParameterModifier>());
}
}
}

View File

@ -0,0 +1,89 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
namespace Microsoft.AspNetCore.Blazor.Hosting
{
internal static class EntrypointInvoker
{
// This method returns void because currently the JS side is not listening to any result,
// nor will it handle any exceptions. We handle all exceptions internally to this method.
// In the future we may want Blazor.start to return something that exposes the possibly-async
// entrypoint result to the JS caller. There's no requirement to do that today, and if we
// do change this it will be non-breaking.
public static void InvokeEntrypoint(string assemblyName, string[] args)
{
object entrypointResult;
try
{
var assembly = Assembly.Load(assemblyName);
var entrypoint = FindUnderlyingEntrypoint(assembly);
var @params = entrypoint.GetParameters().Length == 1 ? new object[] { args ?? Array.Empty<string>() } : new object[] { };
entrypointResult = entrypoint.Invoke(null, @params);
}
catch (Exception syncException)
{
HandleStartupException(syncException);
return;
}
// If the entrypoint is async, handle async exceptions in the same way that we would
// have handled sync ones
if (entrypointResult is Task entrypointTask)
{
entrypointTask.ContinueWith(task =>
{
if (task.Exception != null)
{
HandleStartupException(task.Exception);
}
});
}
}
private static MethodBase FindUnderlyingEntrypoint(Assembly assembly)
{
// This is the entrypoint declared in .NET metadata. In the case of async main, it's the
// compiler-generated wrapper method. Otherwise it's the developer-defined method.
var metadataEntrypointMethodBase = assembly.EntryPoint;
// For "async Task Main", the C# compiler generates a method called "<Main>"
// that is marked as the assembly entrypoint. Detect this case, and instead of
// calling "<Whatever>", call the sibling "Whatever".
if (metadataEntrypointMethodBase.IsSpecialName)
{
var origName = metadataEntrypointMethodBase.Name;
var origNameLength = origName.Length;
if (origNameLength > 2)
{
var candidateMethodName = origName.Substring(1, origNameLength - 2);
var candidateMethod = metadataEntrypointMethodBase.DeclaringType.GetMethod(
candidateMethodName,
BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic,
null,
metadataEntrypointMethodBase.GetParameters().Select(p => p.ParameterType).ToArray(),
null);
if (candidateMethod != null)
{
return candidateMethod;
}
}
}
// Either it's not async main, or for some reason we couldn't locate the underlying entrypoint,
// so use the one from assembly metadata.
return metadataEntrypointMethodBase;
}
private static void HandleStartupException(Exception exception)
{
// Logs to console, and causes the error UI to appear
Console.Error.WriteLine(exception);
}
}
}

View File

@ -1,16 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNetCore.Components.Builder;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.AspNetCore.Blazor.Hosting
{
internal interface IBlazorStartup
{
void ConfigureServices(IServiceCollection services);
void Configure(IComponentsApplicationBuilder app, IServiceProvider services);
}
}

View File

@ -1,34 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.AspNetCore.Blazor.Hosting
{
/// <summary>
/// A program abstraction.
/// </summary>
public interface IWebAssemblyHost : IDisposable
{
/// <summary>
/// The programs configured services.
/// </summary>
IServiceProvider Services { get; }
/// <summary>
/// Start the program.
/// </summary>
/// <param name="cancellationToken">Used to abort program start.</param>
/// <returns></returns>
Task StartAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Attempts to gracefully stop the program.
/// </summary>
/// <param name="cancellationToken">Used to indicate when stop should no longer be graceful.</param>
/// <returns></returns>
Task StopAsync(CancellationToken cancellationToken = default);
}
}

View File

@ -1,46 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.AspNetCore.Blazor.Hosting
{
/// <summary>
/// Abstraction for configuring a Blazor browser-based application.
/// </summary>
public interface IWebAssemblyHostBuilder
{
/// <summary>
/// A central location for sharing state between components during the host building process.
/// </summary>
IDictionary<object, object> Properties { get; }
/// <summary>
/// Overrides the factory used to create the service provider.
/// </summary>
/// <returns>The same instance of the <see cref="IWebAssemblyHostBuilder"/> for chaining.</returns>
IWebAssemblyHostBuilder UseServiceProviderFactory<TContainerBuilder>(IServiceProviderFactory<TContainerBuilder> factory);
/// <summary>
/// Overrides the factory used to create the service provider.
/// </summary>
/// <returns>The same instance of the <see cref="IWebAssemblyHostBuilder"/> for chaining.</returns>
IWebAssemblyHostBuilder UseServiceProviderFactory<TContainerBuilder>(Func<WebAssemblyHostBuilderContext, IServiceProviderFactory<TContainerBuilder>> factory);
/// <summary>
/// Adds services to the container. This can be called multiple times and the results will be additive.
/// </summary>
/// <param name="configureDelegate">The delegate for configuring the <see cref="IServiceCollection"/> that will be used
/// to construct the <see cref="IServiceProvider"/>.</param>
/// <returns>The same instance of the <see cref="IWebAssemblyHostBuilder"/> for chaining.</returns>
IWebAssemblyHostBuilder ConfigureServices(Action<WebAssemblyHostBuilderContext, IServiceCollection> configureDelegate);
/// <summary>
/// Run the given actions to initialize the host. This can only be called once.
/// </summary>
/// <returns>An initialized <see cref="IWebAssemblyHost"/></returns>
IWebAssemblyHost Build();
}
}

View File

@ -1,17 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.AspNetCore.Blazor.Hosting
{
// Equivalent to https://github.com/aspnet/Extensions/blob/master/src/Hosting/Hosting/src/Internal/IServiceFactoryAdapter.cs
internal interface IWebAssemblyServiceFactoryAdapter
{
object CreateBuilder(IServiceCollection services);
IServiceProvider CreateServiceProvider(object containerBuilder);
}
}

View File

@ -0,0 +1,53 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNetCore.Components;
namespace Microsoft.AspNetCore.Blazor.Hosting
{
/// <summary>
/// Defines a mapping between a root <see cref="IComponent"/> and a DOM element selector.
/// </summary>
public readonly struct RootComponentMapping
{
/// <summary>
/// Creates a new instance of <see cref="RootComponentMapping"/> with the provided <paramref name="componentType"/>
/// and <paramref name="selector"/>.
/// </summary>
/// <param name="componentType">The component type. Must implement <see cref="IComponent"/>.</param>
/// <param name="selector">The DOM element selector.</param>
public RootComponentMapping(Type componentType, string selector)
{
if (componentType is null)
{
throw new ArgumentNullException(nameof(componentType));
}
if (!typeof(IComponent).IsAssignableFrom(componentType))
{
throw new ArgumentException(
$"The type '{componentType.Name}' must implement {nameof(IComponent)} to be used as a root component.",
nameof(componentType));
}
if (selector is null)
{
throw new ArgumentNullException(nameof(selector));
}
ComponentType = componentType;
Selector = selector;
}
/// <summary>
/// Gets the component type.
/// </summary>
public Type ComponentType { get; }
/// <summary>
/// Gets the DOM element selector.
/// </summary>
public string Selector { get; }
}
}

View File

@ -0,0 +1,68 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using Microsoft.AspNetCore.Components;
namespace Microsoft.AspNetCore.Blazor.Hosting
{
/// <summary>
/// Defines a collection of <see cref="RootComponentMapping"/> items.
/// </summary>
public class RootComponentMappingCollection : Collection<RootComponentMapping>
{
/// <summary>
/// Adds a component mapping to the collection.
/// </summary>
/// <typeparam name="TComponent">The component type.</typeparam>
/// <param name="selector">The DOM element selector.</param>
public void Add<TComponent>(string selector) where TComponent : IComponent
{
if (selector is null)
{
throw new ArgumentNullException(nameof(selector));
}
Add(new RootComponentMapping(typeof(TComponent), selector));
}
/// <summary>
/// Adds a component mapping to the collection.
/// </summary>
/// <param name="componentType">The component type. Must implement <see cref="IComponent"/>.</param>
/// <param name="selector">The DOM element selector.</param>
public void Add(Type componentType, string selector)
{
if (componentType is null)
{
throw new ArgumentNullException(nameof(componentType));
}
if (selector is null)
{
throw new ArgumentNullException(nameof(selector));
}
Add(new RootComponentMapping(componentType, selector));
}
/// <summary>
/// Adds a collection of items to this collection.
/// </summary>
/// <param name="items">The items to add.</param>
public void AddRange(IEnumerable<RootComponentMapping> items)
{
if (items is null)
{
throw new ArgumentNullException(nameof(items));
}
foreach (var item in items)
{
Add(item);
}
}
}
}

View File

@ -1,54 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Blazor.Rendering;
using Microsoft.AspNetCore.Components.Builder;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
namespace Microsoft.AspNetCore.Blazor.Hosting
{
internal class WebAssemblyBlazorApplicationBuilder : IComponentsApplicationBuilder
{
public WebAssemblyBlazorApplicationBuilder(IServiceProvider services)
{
Entries = new List<(Type componentType, string domElementSelector)>();
Services = services;
}
public List<(Type componentType, string domElementSelector)> Entries { get; }
public IServiceProvider Services { get; }
public void AddComponent(Type componentType, string domElementSelector)
{
if (componentType == null)
{
throw new ArgumentNullException(nameof(componentType));
}
if (domElementSelector == null)
{
throw new ArgumentNullException(nameof(domElementSelector));
}
Entries.Add((componentType, domElementSelector));
}
public async Task<WebAssemblyRenderer> CreateRendererAsync()
{
var loggerFactory = (ILoggerFactory)Services.GetService(typeof(ILoggerFactory));
var renderer = new WebAssemblyRenderer(Services, loggerFactory);
for (var i = 0; i < Entries.Count; i++)
{
var (componentType, domElementSelector) = Entries[i];
await renderer.AddComponentAsync(componentType, domElementSelector);
}
return renderer;
}
}
}

View File

@ -5,89 +5,135 @@ using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Blazor.Rendering;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.JSInterop;
using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Blazor.Hosting
{
internal class WebAssemblyHost : IWebAssemblyHost
/// <summary>
/// A host object for Blazor running under WebAssembly. Use <see cref="WebAssemblyHostBuilder"/>
/// to initialize a <see cref="WebAssemblyHost"/>.
/// </summary>
public sealed class WebAssemblyHost : IAsyncDisposable
{
private readonly IJSRuntime _runtime;
private readonly IServiceScope _scope;
private readonly IServiceProvider _services;
private readonly IConfiguration _configuration;
private readonly RootComponentMapping[] _rootComponents;
private IServiceScope _scope;
// NOTE: the host is disposable because it OWNs references to disposable things.
//
// The twist is that in general dispose is not going to run even if the user puts it in a using.
// When a user refreshes or navigates away that terminates the app, like a process.exit. So the
// dispose functionality here is basically so that it can be used in unit tests.
//
// Based on the APIs that exist in Blazor today it's not possible for the
// app to get disposed, however if we add something like that in the future, most of the work is
// already done.
private bool _disposed;
private bool _started;
private WebAssemblyRenderer _renderer;
public WebAssemblyHost(IServiceProvider services, IJSRuntime runtime)
internal WebAssemblyHost(IServiceProvider services, IServiceScope scope, IConfiguration configuration, RootComponentMapping[] rootComponents)
{
Services = services ?? throw new ArgumentNullException(nameof(services));
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
// To ensure JS-invoked methods don't get linked out, have a reference to their enclosing types
GC.KeepAlive(typeof(EntrypointInvoker));
GC.KeepAlive(typeof(JSInteropMethods));
GC.KeepAlive(typeof(WebAssemblyEventDispatcher));
_services = services;
_scope = scope;
_configuration = configuration;
_rootComponents = rootComponents;
}
public IServiceProvider Services { get; }
/// <summary>
/// Gets the application configuration.
/// </summary>
public IConfiguration Configuration => _configuration;
public Task StartAsync(CancellationToken cancellationToken = default)
/// <summary>
/// Gets the service provider associated with the application.
/// </summary>
public IServiceProvider Services => _scope.ServiceProvider;
/// <summary>
/// Disposes the host asynchronously.
/// </summary>
/// <returns>A <see cref="ValueTask"/> which respresents the completion of disposal.</returns>
public async ValueTask DisposeAsync()
{
return StartAsyncAwaited();
}
private async Task StartAsyncAwaited()
{
var scopeFactory = Services.GetRequiredService<IServiceScopeFactory>();
_scope = scopeFactory.CreateScope();
try
if (_disposed)
{
var startup = _scope.ServiceProvider.GetService<IBlazorStartup>();
if (startup == null)
return;
}
_disposed = true;
_renderer?.Dispose();
if (_scope is IAsyncDisposable asyncDisposableScope)
{
await asyncDisposableScope.DisposeAsync();
}
else
{
_scope?.Dispose();
}
if (_services is IAsyncDisposable asyncDisposableServices)
{
await asyncDisposableServices.DisposeAsync();
}
else if (_services is IDisposable disposableServices)
{
disposableServices.Dispose();
}
}
/// <summary>
/// Runs the application associated with this host.
/// </summary>
/// <returns>A <see cref="Task"/> which represents exit of the application.</returns>
/// <remarks>
/// At this time, it's not possible to shut down a Blazor WebAssembly application using imperative code.
/// The application only stops when the hosting page is reloaded or navigated to another page. As a result
/// the task returned from this method does not complete. This method is not suitable for use in unit-testing.
/// </remarks>
public Task RunAsync()
{
// RunAsyncCore will await until the CancellationToken fires. However, we don't fire it
// currently, so the app will "run" forever.
return RunAsyncCore(CancellationToken.None);
}
// Internal for testing.
internal async Task RunAsyncCore(CancellationToken cancellationToken)
{
if (_started)
{
throw new InvalidOperationException("The host has already started.");
}
_started = true;
var tcs = new TaskCompletionSource<object>();
using (cancellationToken.Register(() => { tcs.TrySetResult(null); }))
{
var loggerFactory = Services.GetRequiredService<ILoggerFactory>();
_renderer = new WebAssemblyRenderer(Services, loggerFactory);
var rootComponents = _rootComponents;
for (var i = 0; i < rootComponents.Length; i++)
{
var message =
$"Could not find a registered Blazor Startup class. " +
$"Using {nameof(IWebAssemblyHost)} requires a call to {nameof(IWebAssemblyHostBuilder)}.UseBlazorStartup.";
throw new InvalidOperationException(message);
var rootComponent = rootComponents[i];
await _renderer.AddComponentAsync(rootComponent.ComponentType, rootComponent.Selector);
}
// Note that we differ from the WebHost startup path here by using a 'scope' for the app builder
// as well as the Configure method.
var builder = new WebAssemblyBlazorApplicationBuilder(_scope.ServiceProvider);
startup.Configure(builder, _scope.ServiceProvider);
_renderer = await builder.CreateRendererAsync();
await tcs.Task;
}
catch
{
_scope.Dispose();
_scope = null;
if (_renderer != null)
{
_renderer.Dispose();
_renderer = null;
}
throw;
}
}
public Task StopAsync(CancellationToken cancellationToken = default)
{
if (_scope != null)
{
_scope.Dispose();
_scope = null;
}
if (_renderer != null)
{
_renderer.Dispose();
_renderer = null;
}
return Task.CompletedTask;
}
public void Dispose()
{
(Services as IDisposable)?.Dispose();
}
}
}

View File

@ -3,10 +3,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using Microsoft.AspNetCore.Blazor.Services;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Routing;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
@ -14,87 +16,89 @@ using Microsoft.JSInterop;
namespace Microsoft.AspNetCore.Blazor.Hosting
{
//
// This code was taken virtually as-is from the Microsoft.Extensions.Hosting project in aspnet/Hosting and then
// lots of things were removed.
//
internal class WebAssemblyHostBuilder : IWebAssemblyHostBuilder
/// <summary>
/// A builder for configuring and creating a <see cref="WebAssemblyHost"/>.
/// </summary>
public sealed class WebAssemblyHostBuilder
{
private List<Action<WebAssemblyHostBuilderContext, IServiceCollection>> _configureServicesActions = new List<Action<WebAssemblyHostBuilderContext, IServiceCollection>>();
private bool _hostBuilt;
private WebAssemblyHostBuilderContext _BrowserHostBuilderContext;
private IWebAssemblyServiceFactoryAdapter _serviceProviderFactory = new WebAssemblyServiceFactoryAdapter<IServiceCollection>(new DefaultServiceProviderFactory());
private IServiceProvider _appServices;
/// <summary>
/// A central location for sharing state between components during the host building process.
/// Creates an instance of <see cref="WebAssemblyHostBuilder"/> using the most common
/// conventions and settings.
/// </summary>
public IDictionary<object, object> Properties { get; } = new Dictionary<object, object>();
/// <summary>
/// Adds services to the container. This can be called multiple times and the results will be additive.
/// </summary>
/// <param name="configureDelegate"></param>
/// <returns>The same instance of the <see cref="IWebAssemblyHostBuilder"/> for chaining.</returns>
public IWebAssemblyHostBuilder ConfigureServices(Action<WebAssemblyHostBuilderContext, IServiceCollection> configureDelegate)
/// <param name="args">The argument passed to the application's main method.</param>
/// <returns>A <see cref="WebAssemblyHostBuilder"/>.</returns>
public static WebAssemblyHostBuilder CreateDefault(string[] args = default)
{
_configureServicesActions.Add(configureDelegate ?? throw new ArgumentNullException(nameof(configureDelegate)));
return this;
// We don't use the args for anything right now, but we want to accept them
// here so that it shows up this way in the project templates.
args ??= Array.Empty<string>();
var builder = new WebAssemblyHostBuilder();
// Right now we don't have conventions or behaviors that are specific to this method
// however, making this the default for the template allows us to add things like that
// in the future, while giving `new WebAssemblyHostBuilder` as an opt-out of opinionated
// settings.
return builder;
}
/// <summary>
/// Overrides the factory used to create the service provider.
/// Creates an instance of <see cref="WebAssemblyHostBuilder"/> with the minimal configuration.
/// </summary>
/// <returns>The same instance of the <see cref="IWebAssemblyHostBuilder"/> for chaining.</returns>
public IWebAssemblyHostBuilder UseServiceProviderFactory<TContainerBuilder>(IServiceProviderFactory<TContainerBuilder> factory)
private WebAssemblyHostBuilder()
{
_serviceProviderFactory = new WebAssemblyServiceFactoryAdapter<TContainerBuilder>(factory ?? throw new ArgumentNullException(nameof(factory)));
return this;
// Private right now because we don't have much reason to expose it. This can be exposed
// in the future if we want to give people a choice between CreateDefault and something
// less opinionated.
Configuration = new ConfigurationBuilder();
RootComponents = new RootComponentMappingCollection();
Services = new ServiceCollection();
InitializeDefaultServices();
}
/// <summary>
/// Overrides the factory used to create the service provider.
/// Gets an <see cref="IConfigurationBuilder"/> that can be used to customize the application's
/// configuration sources.
/// </summary>
/// <returns>The same instance of the <see cref="IWebAssemblyHostBuilder"/> for chaining.</returns>
public IWebAssemblyHostBuilder UseServiceProviderFactory<TContainerBuilder>(Func<WebAssemblyHostBuilderContext, IServiceProviderFactory<TContainerBuilder>> factory)
{
_serviceProviderFactory = new WebAssemblyServiceFactoryAdapter<TContainerBuilder>(() => _BrowserHostBuilderContext, factory ?? throw new ArgumentNullException(nameof(factory)));
return this;
}
public IConfigurationBuilder Configuration { get; }
/// <summary>
/// Run the given actions to initialize the host. This can only be called once.
/// Gets the collection of root component mappings configured for the application.
/// </summary>
/// <returns>An initialized <see cref="IWebAssemblyHost"/></returns>
public IWebAssemblyHost Build()
public RootComponentMappingCollection RootComponents { get; }
/// <summary>
/// Gets the service collection.
/// </summary>
public IServiceCollection Services { get; }
/// <summary>
/// Builds a <see cref="WebAssemblyHost"/> instance based on the configuration of this builder.
/// </summary>
/// <returns>A <see cref="WebAssemblyHost"/> object.</returns>
public WebAssemblyHost Build()
{
if (_hostBuilt)
{
throw new InvalidOperationException("Build can only be called once.");
}
_hostBuilt = true;
// Intentionally overwrite configuration with the one we're creating.
var configuration = Configuration.Build();
Services.AddSingleton<IConfiguration>(configuration);
CreateBrowserHostBuilderContext();
CreateServiceProvider();
// A Blazor application always runs in a scope. Since we want to make it possible for the user
// to configure services inside *that scope* inside their startup code, we create *both* the
// service provider and the scope here.
var services = Services.BuildServiceProvider();
var scope = services.GetRequiredService<IServiceScopeFactory>().CreateScope();
return _appServices.GetRequiredService<IWebAssemblyHost>();
return new WebAssemblyHost(services, scope, configuration, RootComponents.ToArray());
}
private void CreateBrowserHostBuilderContext()
private void InitializeDefaultServices()
{
_BrowserHostBuilderContext = new WebAssemblyHostBuilderContext(Properties);
}
private void CreateServiceProvider()
{
var services = new ServiceCollection();
services.AddSingleton(_BrowserHostBuilderContext);
services.AddSingleton<IWebAssemblyHost, WebAssemblyHost>();
services.AddSingleton<IJSRuntime>(WebAssemblyJSRuntime.Instance);
services.AddSingleton<NavigationManager>(WebAssemblyNavigationManager.Instance);
services.AddSingleton<INavigationInterception>(WebAssemblyNavigationInterception.Instance);
services.AddSingleton<ILoggerFactory, WebAssemblyLoggerFactory>();
services.AddSingleton<HttpClient>(s =>
Services.AddSingleton<IJSRuntime>(WebAssemblyJSRuntime.Instance);
Services.AddSingleton<NavigationManager>(WebAssemblyNavigationManager.Instance);
Services.AddSingleton<INavigationInterception>(WebAssemblyNavigationInterception.Instance);
Services.AddSingleton<ILoggerFactory, WebAssemblyLoggerFactory>();
Services.TryAdd(ServiceDescriptor.Singleton(typeof(ILogger<>), typeof(WebAssemblyConsoleLogger<>)));
Services.AddSingleton<HttpClient>(s =>
{
// Creating the URI helper needs to wait until the JS Runtime is initialized, so defer it.
var navigationManager = s.GetRequiredService<NavigationManager>();
@ -103,20 +107,6 @@ namespace Microsoft.AspNetCore.Blazor.Hosting
BaseAddress = new Uri(navigationManager.BaseUri)
};
});
// Needed for authorization
// However, since authorization isn't on by default, we could consider removing these and
// having a separate services.AddBlazorAuthorization() call that brings in the required services.
services.AddOptions();
services.TryAdd(ServiceDescriptor.Singleton(typeof(ILogger<>), typeof(WebAssemblyConsoleLogger<>)));
foreach (var configureServicesAction in _configureServicesActions)
{
configureServicesAction(_BrowserHostBuilderContext, services);
}
var builder = _serviceProviderFactory.CreateBuilder(services);
_appServices = _serviceProviderFactory.CreateServiceProvider(builder);
}
}
}

View File

@ -1,27 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
namespace Microsoft.AspNetCore.Blazor.Hosting
{
/// <summary>
/// Context containing the common services on the <see cref="IWebAssemblyHost" />. Some properties may be null until set by the <see cref="IWebAssemblyHost" />.
/// </summary>
public sealed class WebAssemblyHostBuilderContext
{
/// <summary>
/// Creates a new <see cref="WebAssemblyHostBuilderContext" />.
/// </summary>
/// <param name="properties">The property collection.</param>
public WebAssemblyHostBuilderContext(IDictionary<object, object> properties)
{
Properties = properties ?? throw new System.ArgumentNullException(nameof(properties));
}
/// <summary>
/// A central location for sharing state between components during the host building process.
/// </summary>
public IDictionary<object, object> Properties { get; }
}
}

View File

@ -1,73 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.AspNetCore.Blazor.Hosting
{
/// <summary>
/// Provides Blazor-specific support for <see cref="IWebAssemblyHost"/>.
/// </summary>
public static class WebAssemblyHostBuilderExtensions
{
private const string BlazorStartupKey = "Blazor.Startup";
/// <summary>
/// Adds services to the container. This can be called multiple times and the results will be additive.
/// </summary>
/// <param name="hostBuilder">The <see cref="IWebAssemblyHostBuilder" /> to configure.</param>
/// <param name="configureDelegate"></param>
/// <returns>The same instance of the <see cref="IWebAssemblyHostBuilder"/> for chaining.</returns>
public static IWebAssemblyHostBuilder ConfigureServices(this IWebAssemblyHostBuilder hostBuilder, Action<IServiceCollection> configureDelegate)
{
return hostBuilder.ConfigureServices((context, collection) => configureDelegate(collection));
}
/// <summary>
/// Configures the <see cref="IWebAssemblyHostBuilder"/> to use the provided startup class.
/// </summary>
/// <param name="builder">The <see cref="IWebAssemblyHostBuilder"/>.</param>
/// <param name="startupType">A type that configures a Blazor application.</param>
/// <returns>The <see cref="IWebAssemblyHostBuilder"/>.</returns>
public static IWebAssemblyHostBuilder UseBlazorStartup(this IWebAssemblyHostBuilder builder, Type startupType)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
if (builder.Properties.ContainsKey(BlazorStartupKey))
{
throw new InvalidOperationException("A startup class has already been registered.");
}
// It would complicate the implementation to allow multiple startup classes, and we don't
// really have a need for it.
builder.Properties.Add(BlazorStartupKey, bool.TrueString);
var startup = new ConventionBasedStartup(Activator.CreateInstance(startupType));
builder.ConfigureServices(startup.ConfigureServices);
builder.ConfigureServices(s => s.AddSingleton<IBlazorStartup>(startup));
return builder;
}
/// <summary>
/// Configures the <see cref="IWebAssemblyHostBuilder"/> to use the provided startup class.
/// </summary>
/// <typeparam name="TStartup">A type that configures a Blazor application.</typeparam>
/// <param name="builder">The <see cref="IWebAssemblyHostBuilder"/>.</param>
/// <returns>The <see cref="IWebAssemblyHostBuilder"/>.</returns>
public static IWebAssemblyHostBuilder UseBlazorStartup<TStartup>(this IWebAssemblyHostBuilder builder)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
return UseBlazorStartup(builder, typeof(TStartup));
}
}
}

View File

@ -1,36 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
namespace Microsoft.AspNetCore.Blazor.Hosting
{
/// <summary>
/// Extension methods for <see cref="IWebAssemblyHost"/>.
/// </summary>
public static class WebAssemblyHostExtensions
{
/// <summary>
/// Runs the application.
/// </summary>
/// <param name="host">The <see cref="IWebAssemblyHost"/> to run.</param>
/// <remarks>
/// Currently, Blazor applications running in the browser don't have a lifecycle - the application does not
/// get a chance to gracefully shut down. For now, <see cref="Run(IWebAssemblyHost)"/> simply starts the host
/// and allows execution to continue.
/// </remarks>
public static void Run(this IWebAssemblyHost host)
{
// Behave like async void, because we don't yet support async-main properly on WebAssembly.
// However, don't actualy make this method async, because we rely on startup being synchronous
// for things like attaching navigation event handlers.
host.StartAsync().ContinueWith(task =>
{
if (task.Exception != null)
{
Console.WriteLine(task.Exception);
}
});
}
}
}

View File

@ -1,52 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.AspNetCore.Blazor.Hosting
{
// Equivalent to https://github.com/aspnet/Extensions/blob/master/src/Hosting/Hosting/src/Internal/ServiceFactoryAdapter.cs
internal class WebAssemblyServiceFactoryAdapter<TContainerBuilder> : IWebAssemblyServiceFactoryAdapter
{
private IServiceProviderFactory<TContainerBuilder> _serviceProviderFactory;
private readonly Func<WebAssemblyHostBuilderContext> _contextResolver;
private Func<WebAssemblyHostBuilderContext, IServiceProviderFactory<TContainerBuilder>> _factoryResolver;
public WebAssemblyServiceFactoryAdapter(IServiceProviderFactory<TContainerBuilder> serviceProviderFactory)
{
_serviceProviderFactory = serviceProviderFactory ?? throw new ArgumentNullException(nameof(serviceProviderFactory));
}
public WebAssemblyServiceFactoryAdapter(Func<WebAssemblyHostBuilderContext> contextResolver, Func<WebAssemblyHostBuilderContext, IServiceProviderFactory<TContainerBuilder>> factoryResolver)
{
_contextResolver = contextResolver ?? throw new ArgumentNullException(nameof(contextResolver));
_factoryResolver = factoryResolver ?? throw new ArgumentNullException(nameof(factoryResolver));
}
public object CreateBuilder(IServiceCollection services)
{
if (_serviceProviderFactory == null)
{
_serviceProviderFactory = _factoryResolver(_contextResolver());
if (_serviceProviderFactory == null)
{
throw new InvalidOperationException("The resolver returned a null IServiceProviderFactory");
}
}
return _serviceProviderFactory.CreateBuilder(services);
}
public IServiceProvider CreateServiceProvider(object containerBuilder)
{
if (_serviceProviderFactory == null)
{
throw new InvalidOperationException("CreateBuilder must be called before CreateServiceProvider");
}
return _serviceProviderFactory.CreateServiceProvider((TContainerBuilder)containerBuilder);
}
}
}

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<TargetFramework>netstandard2.1</TargetFramework>
<Description>Build client-side single-page applications (SPAs) with Blazor running under WebAssembly.</Description>
<IsShippingPackage>false</IsShippingPackage>
</PropertyGroup>
@ -9,7 +9,7 @@
<ItemGroup>
<Reference Include="Mono.WebAssembly.Interop" />
<Reference Include="Microsoft.AspNetCore.Components.Web" />
<Reference Include="Microsoft.Extensions.Options" />
<Reference Include="Microsoft.Extensions.Configuration" />
</ItemGroup>
<ItemGroup>

View File

@ -1,210 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Blazor.Hosting;
using Microsoft.AspNetCore.Components.Builder;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
namespace Microsoft.AspNetCore.Components.Hosting
{
public class ConventionBasedStartupTest
{
[Fact]
public void ConventionBasedStartup_GetConfigureServicesMethod_FindsConfigureServices()
{
// Arrange
var startup = new ConventionBasedStartup(new MyStartup1());
// Act
var method = startup.GetConfigureServicesMethod();
// Assert
Assert.Equal(typeof(IServiceCollection), method.GetParameters()[0].ParameterType);
}
private class MyStartup1
{
public void ConfigureServices(IServiceCollection services)
{
}
// Ignored
public void ConfigureServices(DateTime x)
{
}
// Ignored
private void ConfigureServices(int x)
{
}
// Ignored
public static void ConfigureServices(string x)
{
}
}
[Fact]
public void ConventionBasedStartup_GetConfigureServicesMethod_NoMethodFound()
{
// Arrange
var startup = new ConventionBasedStartup(new MyStartup2());
// Act
var method = startup.GetConfigureServicesMethod();
// Assert
Assert.Null(method);
}
private class MyStartup2
{
}
[Fact]
public void ConventionBasedStartup_ConfigureServices_CallsMethod()
{
// Arrange
var startup = new ConventionBasedStartup(new MyStartup3());
var services = new ServiceCollection();
// Act
startup.ConfigureServices(services);
// Assert
Assert.NotEmpty(services);
}
private class MyStartup3
{
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton("foo");
}
}
[Fact]
public void ConventionBasedStartup_ConfigureServices_NoMethodFound()
{
// Arrange
var startup = new ConventionBasedStartup(new MyStartup4());
var services = new ServiceCollection();
// Act
startup.ConfigureServices(services);
// Assert
Assert.Empty(services);
}
private class MyStartup4
{
}
[Fact]
public void ConventionBasedStartup_GetConfigureMethod_FindsConfigure()
{
// Arrange
var startup = new ConventionBasedStartup(new MyStartup5());
// Act
var method = startup.GetConfigureMethod();
// Assert
Assert.Empty(method.GetParameters());
}
private class MyStartup5
{
public void Configure()
{
}
// Ignored
private void Configure(int x)
{
}
// Ignored
public static void Configure(string x)
{
}
}
[Fact]
public void ConventionBasedStartup_GetConfigureMethod_NoMethodFoundThrows()
{
// Arrange
var startup = new ConventionBasedStartup(new MyStartup6());
// Act
var ex = Assert.Throws<InvalidOperationException>(() => startup.GetConfigureMethod());
// Assert
Assert.Equal("The startup class must define a 'Configure' method.", ex.Message);
}
private class MyStartup6
{
}
[Fact]
public void ConventionBasedStartup_GetConfigureMethod_OverloadedThrows()
{
// Arrange
var startup = new ConventionBasedStartup(new MyStartup7());
// Act
var ex = Assert.Throws<InvalidOperationException>(() => startup.GetConfigureMethod());
// Assert
Assert.Equal("Overloading the 'Configure' method is not supported.", ex.Message);
}
private class MyStartup7
{
public void Configure()
{
}
public void Configure(string x)
{
}
}
[Fact]
public void ConventionBasedStartup_Configure()
{
// Arrange
var instance = new MyStartup8();
var startup = new ConventionBasedStartup(instance);
var services = new ServiceCollection().AddSingleton("foo").BuildServiceProvider();
var builder = new WebAssemblyBlazorApplicationBuilder(services);
// Act
startup.Configure(builder, services);
// Assert
Assert.Collection(
instance.Arguments,
a => Assert.Same(builder, a),
a => Assert.Equal("foo", a));
}
private class MyStartup8
{
public List<object> Arguments { get; } = new List<object>();
public void Configure(IComponentsApplicationBuilder app, string foo)
{
Arguments.Add(app);
Arguments.Add(foo);
}
}
}
}

View File

@ -0,0 +1,153 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.IO;
using System.Reflection;
using System.Runtime.Loader;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Xunit;
namespace Microsoft.AspNetCore.Blazor.Hosting
{
public class EntrypointInvokerTest
{
[Theory]
[InlineData(false, false)]
[InlineData(false, true)]
[InlineData(true, false)]
[InlineData(true, true)]
public void InvokesEntrypoint_Sync_Success(bool hasReturnValue, bool hasParams)
{
// Arrange
var returnType = hasReturnValue ? "int" : "void";
var paramsDecl = hasParams ? "string[] args" : string.Empty;
var returnStatement = hasReturnValue ? "return 123;" : "return;";
var assembly = CompileToAssembly(@"
static " + returnType + @" Main(" + paramsDecl + @")
{
DidMainExecute = true;
" + returnStatement + @"
}", out var didMainExecute);
// Act
EntrypointInvoker.InvokeEntrypoint(assembly.FullName, new string[] { });
// Assert
Assert.True(didMainExecute());
}
[Theory]
[InlineData(false, false)]
[InlineData(false, true)]
[InlineData(true, false)]
[InlineData(true, true)]
public void InvokesEntrypoint_Async_Success(bool hasReturnValue, bool hasParams)
{
// Arrange
var returnTypeGenericParam = hasReturnValue ? "<int>" : string.Empty;
var paramsDecl = hasParams ? "string[] args" : string.Empty;
var returnStatement = hasReturnValue ? "return 123;" : "return;";
var assembly = CompileToAssembly(@"
public static TaskCompletionSource<object> ContinueTcs { get; } = new TaskCompletionSource<object>();
static async Task" + returnTypeGenericParam + @" Main(" + paramsDecl + @")
{
await ContinueTcs.Task;
DidMainExecute = true;
" + returnStatement + @"
}", out var didMainExecute);
// Act/Assert 1: Waits for task
// The fact that we're not blocking here proves that we're not executing the
// metadata-declared entrypoint, as that would block
EntrypointInvoker.InvokeEntrypoint(assembly.FullName, new string[] { });
Assert.False(didMainExecute());
// Act/Assert 2: Continues
var tcs = (TaskCompletionSource<object>)assembly.GetType("SomeApp.Program").GetProperty("ContinueTcs").GetValue(null);
tcs.SetResult(null);
Assert.True(didMainExecute());
}
[Fact]
public void InvokesEntrypoint_Sync_Exception()
{
// Arrange
var assembly = CompileToAssembly(@"
public static void Main()
{
DidMainExecute = true;
throw new InvalidTimeZoneException(""Test message"");
}", out var didMainExecute);
// Act/Assert
// The fact that this doesn't throw shows that EntrypointInvoker is doing something
// to handle the exception. We can't assert about what it does here, because that
// would involve capturing console output, which isn't safe in unit tests. Instead
// we'll check this in E2E tests.
EntrypointInvoker.InvokeEntrypoint(assembly.FullName, new string[] { });
Assert.True(didMainExecute());
}
[Fact]
public void InvokesEntrypoint_Async_Exception()
{
// Arrange
var assembly = CompileToAssembly(@"
public static TaskCompletionSource<object> ContinueTcs { get; } = new TaskCompletionSource<object>();
public static async Task Main()
{
await ContinueTcs.Task;
DidMainExecute = true;
throw new InvalidTimeZoneException(""Test message"");
}", out var didMainExecute);
// Act/Assert 1: Waits for task
EntrypointInvoker.InvokeEntrypoint(assembly.FullName, new string[] { });
Assert.False(didMainExecute());
// Act/Assert 2: Continues
// As above, we can't directly observe the exception handling behavior here,
// so this is covered in E2E tests instead.
var tcs = (TaskCompletionSource<object>)assembly.GetType("SomeApp.Program").GetProperty("ContinueTcs").GetValue(null);
tcs.SetResult(null);
Assert.True(didMainExecute());
}
private static Assembly CompileToAssembly(string mainMethod, out Func<bool> didMainExecute)
{
var syntaxTree = CSharpSyntaxTree.ParseText(@"
using System;
using System.Threading.Tasks;
namespace SomeApp
{
public static class Program
{
public static bool DidMainExecute { get; private set; }
" + mainMethod + @"
}
}");
var compilation = CSharpCompilation.Create(
$"TestAssembly-{Guid.NewGuid().ToString("D")}",
new[] { syntaxTree },
new[] { MetadataReference.CreateFromFile(typeof(object).GetTypeInfo().Assembly.Location) },
new CSharpCompilationOptions(OutputKind.ConsoleApplication));
using var ms = new MemoryStream();
var compilationResult = compilation.Emit(ms);
ms.Seek(0, SeekOrigin.Begin);
var assembly = AssemblyLoadContext.Default.LoadFromStream(ms);
var didMainExecuteProp = assembly.GetType("SomeApp.Program").GetProperty("DidMainExecute");
didMainExecute = () => (bool)didMainExecuteProp.GetValue(null);
return assembly;
}
}
}

View File

@ -0,0 +1,37 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Text;
using Microsoft.AspNetCore.Components.Routing;
using Microsoft.AspNetCore.Testing;
using Xunit;
namespace Microsoft.AspNetCore.Blazor.Hosting
{
public class RootComponentMappingTest
{
[Fact]
public void Constructor_ValidatesComponentType_Success()
{
// Arrange
// Act
var mapping = new RootComponentMapping(typeof(Router), "test");
// Assert (does not throw)
GC.KeepAlive(mapping);
}
[Fact]
public void Constructor_ValidatesComponentType_Failure()
{
// Arrange
// Act & Assert
ExceptionAssert.ThrowsArgument(
() => new RootComponentMapping(typeof(StringBuilder), "test"),
"componentType",
$"The type '{nameof(StringBuilder)}' must implement IComponent to be used as a root component.");
}
}
}

View File

@ -3,160 +3,100 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Routing;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.JSInterop;
using Xunit;
namespace Microsoft.AspNetCore.Blazor.Hosting.Test
namespace Microsoft.AspNetCore.Blazor.Hosting
{
public class WebAssemblyHostBuilderTest
{
[Fact]
public void HostBuilder_CanCallBuild_BuildsServices()
public void Build_AllowsConfiguringConfiguration()
{
// Arrange
var builder = new WebAssemblyHostBuilder();
var builder = WebAssemblyHostBuilder.CreateDefault();
// Act
var host = builder.Build();
// Assert
Assert.NotNull(host.Services.GetService(typeof(IWebAssemblyHost)));
}
[Fact]
public void HostBuilder_CanConfigureAdditionalServices()
{
// Arrange
var builder = new WebAssemblyHostBuilder();
builder.ConfigureServices((c, s) => s.AddSingleton<string>("foo"));
builder.ConfigureServices((c, s) => s.AddSingleton<StringBuilder>(new StringBuilder("bar")));
// Act
var host = builder.Build();
// Assert
Assert.Equal("foo", host.Services.GetService(typeof(string)));
Assert.Equal("bar", host.Services.GetService(typeof(StringBuilder)).ToString());
}
[Fact]
public void HostBuilder_UseBlazorStartup_CanConfigureAdditionalServices()
{
// Arrange
var builder = new WebAssemblyHostBuilder();
builder.UseBlazorStartup<MyStartup>();
builder.ConfigureServices((c, s) => s.AddSingleton<StringBuilder>(new StringBuilder("bar")));
// Act
var host = builder.Build();
// Assert
Assert.Equal("foo", host.Services.GetService(typeof(string)));
Assert.Equal("bar", host.Services.GetService(typeof(StringBuilder)).ToString());
}
[Fact]
public void HostBuilder_UseBlazorStartup_DoesNotAllowMultiple()
{
// Arrange
var builder = new WebAssemblyHostBuilder();
builder.UseBlazorStartup<MyStartup>();
// Act
var ex = Assert.Throws<InvalidOperationException>(() => builder.UseBlazorStartup<MyStartup>());
// Assert
Assert.Equal("A startup class has already been registered.", ex.Message);
}
private class MyStartup
{
public void ConfigureServices(IServiceCollection services)
builder.Configuration.AddInMemoryCollection(new[]
{
services.AddSingleton<string>("foo");
}
}
[Fact]
public void HostBuilder_CanCustomizeServiceFactory()
{
// Arrange
var builder = new WebAssemblyHostBuilder();
builder.UseServiceProviderFactory(new TestServiceProviderFactory());
// Act
var host = builder.Build();
// Assert
Assert.IsType<TestServiceProvider>(host.Services);
}
[Fact]
public void HostBuilder_CanCustomizeServiceFactoryWithContext()
{
// Arrange
var builder = new WebAssemblyHostBuilder();
builder.UseServiceProviderFactory(context =>
{
Assert.NotNull(context.Properties);
Assert.Same(builder.Properties, context.Properties);
return new TestServiceProviderFactory();
new KeyValuePair<string, string>("key", "value"),
});
// Act
var host = builder.Build();
// Assert
Assert.IsType<TestServiceProvider>(host.Services);
Assert.Equal("value", host.Configuration["key"]);
}
private class TestServiceProvider : IServiceProvider
[Fact]
public void Build_AllowsConfiguringServices()
{
private readonly IServiceProvider _underlyingProvider;
// Arrange
var builder = WebAssemblyHostBuilder.CreateDefault();
public TestServiceProvider(IServiceProvider underlyingProvider)
{
_underlyingProvider = underlyingProvider;
}
// This test also verifies that we create a scope.
builder.Services.AddScoped<StringBuilder>();
public object GetService(Type serviceType)
// Act
var host = builder.Build();
// Assert
Assert.NotNull(host.Services.GetRequiredService<StringBuilder>());
}
[Fact]
public void Build_AddsConfigurationToServices()
{
// Arrange
var builder = WebAssemblyHostBuilder.CreateDefault();
builder.Configuration.AddInMemoryCollection(new[]
{
if (serviceType == typeof(IWebAssemblyHost))
new KeyValuePair<string, string>("key", "value"),
});
// Act
var host = builder.Build();
// Assert
var configuration = host.Services.GetRequiredService<IConfiguration>();
Assert.Equal("value", configuration["key"]);
}
private static IReadOnlyList<Type> DefaultServiceTypes
{
get
{
return new Type[]
{
// Since the test will make assertions about the resulting IWebAssemblyHost,
// show that custom DI containers have the power to substitute themselves
// as the IServiceProvider
return new WebAssemblyHost(
this, _underlyingProvider.GetRequiredService<IJSRuntime>());
}
else
{
return _underlyingProvider.GetService(serviceType);
}
typeof(IJSRuntime),
typeof(NavigationManager),
typeof(INavigationInterception),
typeof(ILoggerFactory),
typeof(HttpClient),
typeof(ILogger<>),
};
}
}
private class TestServiceProviderFactory : IServiceProviderFactory<IServiceCollection>
[Fact]
public void Constructor_AddsDefaultServices()
{
public IServiceCollection CreateBuilder(IServiceCollection services)
{
return new TestServiceCollection(services);
}
// Arrange & Act
var builder = WebAssemblyHostBuilder.CreateDefault();
public IServiceProvider CreateServiceProvider(IServiceCollection serviceCollection)
// Assert
Assert.Equal(DefaultServiceTypes.Count, builder.Services.Count);
foreach (var type in DefaultServiceTypes)
{
Assert.IsType<TestServiceCollection>(serviceCollection);
return new TestServiceProvider(serviceCollection.BuildServiceProvider());
}
class TestServiceCollection : List<ServiceDescriptor>, IServiceCollection
{
public TestServiceCollection(IEnumerable<ServiceDescriptor> collection)
: base(collection)
{
}
Assert.Single(builder.Services, d => d.ServiceType == type);
}
}
}

View File

@ -2,64 +2,89 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Builder;
using Microsoft.AspNetCore.Components.Hosting;
using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.JSInterop;
using Mono.WebAssembly.Interop;
using Xunit;
namespace Microsoft.AspNetCore.Blazor.Hosting.Test
namespace Microsoft.AspNetCore.Blazor.Hosting
{
public class WebAssemblyHostTest
{
[Fact]
public async Task BrowserHost_StartAsync_ThrowsWithoutStartup()
// This won't happen in the product code, but we need to be able to safely call RunAsync
// to be able to test a few of the other details.
[Fact]
public async Task RunAsync_CanExitBasedOnCancellationToken()
{
// Arrange
var builder = new WebAssemblyHostBuilder();
var builder = WebAssemblyHostBuilder.CreateDefault();
var host = builder.Build();
// Act
var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () => await host.StartAsync());
var cts = new CancellationTokenSource();
// Assert
Assert.Equal(
"Could not find a registered Blazor Startup class. " +
"Using IWebAssemblyHost requires a call to IWebAssemblyHostBuilder.UseBlazorStartup.",
ex.Message);
// Act
var task = host.RunAsyncCore(cts.Token);
cts.Cancel();
await task.TimeoutAfter(TimeSpan.FromSeconds(3));
// Assert (does not throw)
}
[Fact]
public async Task BrowserHost_StartAsync_RunsConfigureMethod()
public async Task RunAsync_CallingTwiceCausesException()
{
// Arrange
var builder = new WebAssemblyHostBuilder();
var startup = new MockStartup();
builder.ConfigureServices((c, s) => { s.AddSingleton<IBlazorStartup>(startup); });
var builder = WebAssemblyHostBuilder.CreateDefault();
var host = builder.Build();
var cts = new CancellationTokenSource();
var task = host.RunAsyncCore(cts.Token);
// Act
await host.StartAsync();
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => host.RunAsyncCore(cts.Token));
cts.Cancel();
await task.TimeoutAfter(TimeSpan.FromSeconds(3));
// Assert
Assert.True(startup.ConfigureCalled);
Assert.Equal("The host has already started.", ex.Message);
}
private class MockStartup : IBlazorStartup
[Fact]
public async Task DisposeAsync_CanDisposeAfterCallingRunAsync()
{
public bool ConfigureCalled { get; set; }
// Arrange
var builder = WebAssemblyHostBuilder.CreateDefault();
builder.Services.AddSingleton<DisposableService>();
var host = builder.Build();
public void Configure(IComponentsApplicationBuilder app, IServiceProvider services)
var disposable = host.Services.GetRequiredService<DisposableService>();
var cts = new CancellationTokenSource();
// Act
await using (host)
{
ConfigureCalled = true;
var task = host.RunAsyncCore(cts.Token);
cts.Cancel();
await task.TimeoutAfter(TimeSpan.FromSeconds(3));
}
public void ConfigureServices(IServiceCollection services)
// Assert
Assert.Equal(1, disposable.DisposeCount);
}
private class DisposableService : IAsyncDisposable
{
public int DisposeCount { get; private set; }
public ValueTask DisposeAsync()
{
DisposeCount++;
return new ValueTask(Task.CompletedTask);
}
}
}

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>

View File

@ -1,61 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.IO;
using Microsoft.Extensions.CommandLineUtils;
namespace Microsoft.AspNetCore.Blazor.Build.DevServer.Commands
{
class ResolveRuntimeDependenciesCommand
{
public static void Command(CommandLineApplication command)
{
var referencesFile = command.Option("--references",
"The path to a file that lists the paths to given referenced dll files",
CommandOptionType.SingleValue);
var baseClassLibrary = command.Option("--base-class-library",
"Full path to a directory in which BCL assemblies can be found",
CommandOptionType.MultipleValue);
var outputPath = command.Option("--output",
"Path to the output file that will contain the list with the full paths of the resolved assemblies",
CommandOptionType.SingleValue);
var mainAssemblyPath = command.Argument("assembly",
"Path to the assembly containing the entry point of the application.");
command.OnExecute(() =>
{
if (string.IsNullOrEmpty(mainAssemblyPath.Value) ||
!baseClassLibrary.HasValue() || !outputPath.HasValue())
{
command.ShowHelp(command.Name);
return 1;
}
try
{
var referencesSources = referencesFile.HasValue()
? File.ReadAllLines(referencesFile.Value())
: Array.Empty<string>();
RuntimeDependenciesResolver.ResolveRuntimeDependencies(
mainAssemblyPath.Value,
referencesSources,
baseClassLibrary.Values.ToArray(),
outputPath.Value());
return 0;
}
catch (Exception ex)
{
Console.WriteLine($"ERROR: {ex.Message}");
Console.WriteLine(ex.StackTrace);
return 1;
}
});
}
}
}

View File

@ -1,68 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.Extensions.CommandLineUtils;
using System;
using System.IO;
namespace Microsoft.AspNetCore.Blazor.Build.DevServer.Commands
{
internal class WriteBootJsonCommand
{
public static void Command(CommandLineApplication command)
{
var referencesFile = command.Option("--references",
"The path to a file that lists the paths to given referenced dll files",
CommandOptionType.SingleValue);
var embeddedResourcesFile = command.Option("--embedded-resources",
"The path to a file that lists the paths of .NET assemblies that may contain embedded resources (typically, referenced assemblies in their pre-linked states)",
CommandOptionType.SingleValue);
var outputPath = command.Option("--output",
"Path to the output file",
CommandOptionType.SingleValue);
var mainAssemblyPath = command.Argument("assembly",
"Path to the assembly containing the entry point of the application.");
var linkerEnabledFlag = command.Option("--linker-enabled",
"If set, specifies that the application is being built with linking enabled.",
CommandOptionType.NoValue);
command.OnExecute(() =>
{
if (string.IsNullOrEmpty(mainAssemblyPath.Value) || !outputPath.HasValue())
{
command.ShowHelp(command.Name);
return 1;
}
try
{
var referencesSources = referencesFile.HasValue()
? File.ReadAllLines(referencesFile.Value())
: Array.Empty<string>();
var embeddedResourcesSources = embeddedResourcesFile.HasValue()
? File.ReadAllLines(embeddedResourcesFile.Value())
: Array.Empty<string>();
BootJsonWriter.WriteFile(
mainAssemblyPath.Value,
referencesSources,
embeddedResourcesSources,
linkerEnabledFlag.HasValue(),
outputPath.Value());
return 0;
}
catch (Exception ex)
{
Console.WriteLine($"ERROR: {ex.Message}");
Console.WriteLine(ex.StackTrace);
return 1;
}
});
}
}
}

View File

@ -1,33 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Blazor.Build.DevServer.Commands;
using Microsoft.Extensions.CommandLineUtils;
namespace Microsoft.AspNetCore.Blazor.Build
{
static class Program
{
static int Main(string[] args)
{
var app = new CommandLineApplication
{
Name = "Microsoft.AspNetCore.Blazor.Build"
};
app.HelpOption("-?|-h|--help");
app.Command("resolve-dependencies", ResolveRuntimeDependenciesCommand.Command);
app.Command("write-boot-json", WriteBootJsonCommand.Command);
if (args.Length > 0)
{
return app.Execute(args);
}
else
{
app.ShowHelp();
return 0;
}
}
}
}

View File

@ -1,95 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using Microsoft.AspNetCore.Components;
using Mono.Cecil;
namespace Microsoft.AspNetCore.Blazor.Build
{
internal class BootJsonWriter
{
public static void WriteFile(
string assemblyPath,
string[] assemblyReferences,
string[] embeddedResourcesSources,
bool linkerEnabled,
string outputPath)
{
var embeddedContent = EmbeddedResourcesProcessor.ExtractEmbeddedResources(
embeddedResourcesSources, Path.GetDirectoryName(outputPath));
var bootJsonText = GetBootJsonContent(
Path.GetFileName(assemblyPath),
GetAssemblyEntryPoint(assemblyPath),
assemblyReferences,
embeddedContent,
linkerEnabled);
var normalizedOutputPath = Path.GetFullPath(outputPath);
Console.WriteLine("Writing boot data to: " + normalizedOutputPath);
File.WriteAllText(normalizedOutputPath, bootJsonText);
}
public static string GetBootJsonContent(string assemblyFileName, string entryPoint, string[] assemblyReferences, IEnumerable<EmbeddedResourceInfo> embeddedContent, bool linkerEnabled)
{
var data = new BootJsonData(
assemblyFileName,
entryPoint,
assemblyReferences,
embeddedContent,
linkerEnabled);
return JsonSerializer.Serialize(data, JsonSerializerOptionsProvider.Options);
}
private static string GetAssemblyEntryPoint(string assemblyPath)
{
using (var assemblyDefinition = AssemblyDefinition.ReadAssembly(assemblyPath))
{
var entryPoint = assemblyDefinition.EntryPoint;
if (entryPoint == null)
{
throw new ArgumentException($"The assembly at {assemblyPath} has no specified entry point.");
}
return $"{entryPoint.DeclaringType.FullName}::{entryPoint.Name}";
}
}
/// <summary>
/// Defines the structure of a Blazor boot JSON file
/// </summary>
class BootJsonData
{
public string Main { get; }
public string EntryPoint { get; }
public IEnumerable<string> AssemblyReferences { get; }
public IEnumerable<string> CssReferences { get; }
public IEnumerable<string> JsReferences { get; }
public bool LinkerEnabled { get; }
public BootJsonData(
string entrypointAssemblyWithExtension,
string entryPoint,
IEnumerable<string> assemblyReferences,
IEnumerable<EmbeddedResourceInfo> embeddedContent,
bool linkerEnabled)
{
Main = entrypointAssemblyWithExtension;
EntryPoint = entryPoint;
AssemblyReferences = assemblyReferences;
LinkerEnabled = linkerEnabled;
CssReferences = embeddedContent
.Where(c => c.Kind == EmbeddedResourceKind.Css)
.Select(c => c.RelativePath);
JsReferences = embeddedContent
.Where(c => c.Kind == EmbeddedResourceKind.JavaScript)
.Select(c => c.RelativePath);
}
}
}
}

View File

@ -1,17 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNetCore.Blazor.Build
{
internal class EmbeddedResourceInfo
{
public EmbeddedResourceKind Kind { get; }
public string RelativePath { get; }
public EmbeddedResourceInfo(EmbeddedResourceKind kind, string relativePath)
{
Kind = kind;
RelativePath = relativePath;
}
}
}

View File

@ -1,12 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNetCore.Blazor.Build
{
internal enum EmbeddedResourceKind
{
JavaScript,
Css,
Static
}
}

View File

@ -1,137 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Mono.Cecil;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace Microsoft.AspNetCore.Blazor.Build
{
internal class EmbeddedResourcesProcessor
{
const string ContentSubdirName = "_content";
private readonly static Dictionary<string, EmbeddedResourceKind> _knownResourceKindsByNamePrefix = new Dictionary<string, EmbeddedResourceKind>
{
{ "blazor:js:", EmbeddedResourceKind.JavaScript },
{ "blazor:css:", EmbeddedResourceKind.Css },
{ "blazor:file:", EmbeddedResourceKind.Static },
};
/// <summary>
/// Finds Blazor-specific embedded resources in the specified assemblies, writes them
/// to disk, and returns a description of those resources in dependency order.
/// </summary>
/// <param name="referencedAssemblyPaths">The paths to assemblies that may contain embedded resources.</param>
/// <param name="outputDir">The path to the directory where output is being written.</param>
/// <returns>A description of the embedded resources that were written to disk.</returns>
public static IReadOnlyList<EmbeddedResourceInfo> ExtractEmbeddedResources(
IEnumerable<string> referencedAssemblyPaths, string outputDir)
{
// Clean away any earlier state
var contentDir = Path.Combine(outputDir, ContentSubdirName);
if (Directory.Exists(contentDir))
{
Directory.Delete(contentDir, recursive: true);
}
// First, get an ordered list of AssemblyDefinition instances
var referencedAssemblyDefinitions = referencedAssemblyPaths
.Where(path => !Path.GetFileName(path).StartsWith("System.", StringComparison.Ordinal)) // Skip System.* because they are never going to contain embedded resources that we want
.Select(path => AssemblyDefinition.ReadAssembly(path))
.ToList();
referencedAssemblyDefinitions.Sort(OrderWithReferenceSubjectFirst);
// Now process them in turn
return referencedAssemblyDefinitions
.SelectMany(def => ExtractEmbeddedResourcesFromSingleAssembly(def, outputDir))
.ToList()
.AsReadOnly();
}
private static IEnumerable<EmbeddedResourceInfo> ExtractEmbeddedResourcesFromSingleAssembly(
AssemblyDefinition assemblyDefinition, string outputDirPath)
{
var assemblyName = assemblyDefinition.Name.Name;
foreach (var res in assemblyDefinition.MainModule.Resources)
{
if (TryExtractEmbeddedResource(assemblyName, res, outputDirPath, out var extractedResourceInfo))
{
yield return extractedResourceInfo;
}
}
}
private static bool TryExtractEmbeddedResource(string assemblyName, Resource resource, string outputDirPath, out EmbeddedResourceInfo extractedResourceInfo)
{
if (resource is EmbeddedResource embeddedResource)
{
if (TryInterpretLogicalName(resource.Name, out var kind, out var name))
{
// Prefix the output path with the assembly name to ensure no clashes
// Also be invariant to the OS on which the package was built
name = Path.Combine(ContentSubdirName, assemblyName, EnsureHasPathSeparators(name, Path.DirectorySeparatorChar));
// Write the file content to disk, ensuring we don't try to write outside the output root
var outputPath = Path.GetFullPath(Path.Combine(outputDirPath, name));
if (!outputPath.StartsWith(outputDirPath))
{
throw new InvalidOperationException($"Cannot write embedded resource from assembly '{assemblyName}' to '{outputPath}' because it is outside the expected directory {outputDirPath}");
}
WriteResourceFile(embeddedResource, outputPath);
// The URLs we write into the boot json file need to use web-style directory separators
extractedResourceInfo = new EmbeddedResourceInfo(kind, EnsureHasPathSeparators(name, '/'));
return true;
}
}
extractedResourceInfo = null;
return false;
}
private static void WriteResourceFile(EmbeddedResource resource, string outputPath)
{
Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
using (var outputStream = File.OpenWrite(outputPath))
{
resource.GetResourceStream().CopyTo(outputStream);
}
}
private static string EnsureHasPathSeparators(string name, char desiredSeparatorChar) => name
.Replace('\\', desiredSeparatorChar)
.Replace('/', desiredSeparatorChar);
private static bool TryInterpretLogicalName(string logicalName, out EmbeddedResourceKind kind, out string resolvedName)
{
foreach (var kvp in _knownResourceKindsByNamePrefix)
{
if (logicalName.StartsWith(kvp.Key, StringComparison.Ordinal))
{
kind = kvp.Value;
resolvedName = logicalName.Substring(kvp.Key.Length);
return true;
}
}
kind = default;
resolvedName = default;
return false;
}
// For each assembly B that references A, we want the resources from A to be loaded before
// the references for B (because B's resources might depend on A's resources)
private static int OrderWithReferenceSubjectFirst(AssemblyDefinition a, AssemblyDefinition b)
=> AssemblyHasReference(a, b) ? 1
: AssemblyHasReference(b, a) ? -1
: 0;
private static bool AssemblyHasReference(AssemblyDefinition from, AssemblyDefinition to)
=> from.MainModule.AssemblyReferences
.Select(reference => reference.Name)
.Contains(to.Name.Name, StringComparer.Ordinal);
}
}

View File

@ -1,165 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using Mono.Cecil;
namespace Microsoft.AspNetCore.Blazor.Build
{
internal class RuntimeDependenciesResolver
{
public static void ResolveRuntimeDependencies(
string entryPoint,
string[] applicationDependencies,
string[] monoBclDirectories,
string outputFile)
{
var paths = ResolveRuntimeDependenciesCore(entryPoint, applicationDependencies, monoBclDirectories);
File.WriteAllLines(outputFile, paths);
}
public static IEnumerable<string> ResolveRuntimeDependenciesCore(
string entryPoint,
string[] applicationDependencies,
string[] monoBclDirectories)
{
var assembly = new AssemblyEntry(entryPoint, AssemblyDefinition.ReadAssembly(entryPoint));
var dependencies = applicationDependencies
.Select(a => new AssemblyEntry(a, AssemblyDefinition.ReadAssembly(a)))
.ToArray();
var bcl = monoBclDirectories
.SelectMany(d => Directory.EnumerateFiles(d, "*.dll").Select(f => Path.Combine(d, f)))
.Select(a => new AssemblyEntry(a, AssemblyDefinition.ReadAssembly(a)))
.ToArray();
var assemblyResolutionContext = new AssemblyResolutionContext(
assembly,
dependencies,
bcl);
assemblyResolutionContext.ResolveAssemblies();
var paths = assemblyResolutionContext.Results.Select(r => r.Path);
return paths.Concat(FindPdbs(paths));
}
private static IEnumerable<string> FindPdbs(IEnumerable<string> dllPaths)
{
return dllPaths
.Select(path => Path.ChangeExtension(path, "pdb"))
.Where(path => File.Exists(path));
}
public class AssemblyResolutionContext
{
public AssemblyResolutionContext(
AssemblyEntry assembly,
AssemblyEntry[] dependencies,
AssemblyEntry[] bcl)
{
Assembly = assembly;
Dependencies = dependencies;
Bcl = bcl;
}
public AssemblyEntry Assembly { get; }
public AssemblyEntry[] Dependencies { get; }
public AssemblyEntry[] Bcl { get; }
public IList<AssemblyEntry> Results { get; } = new List<AssemblyEntry>();
internal void ResolveAssemblies()
{
var visitedAssemblies = new HashSet<string>();
var pendingAssemblies = new Stack<AssemblyNameReference>();
pendingAssemblies.Push(Assembly.Definition.Name);
ResolveAssembliesCore();
void ResolveAssembliesCore()
{
while (pendingAssemblies.TryPop(out var current))
{
if (!visitedAssemblies.Contains(current.Name))
{
visitedAssemblies.Add(current.Name);
// Not all references will be resolvable within the Mono BCL, particularly
// when building for server-side Blazor as you will be running on CoreCLR
// and therefore may depend on System.* BCL assemblies that aren't present
// in Mono WebAssembly. Skipping unresolved assemblies here is equivalent
// to passing "--skip-unresolved true" to the Mono linker.
var resolved = Resolve(current);
if (resolved != null)
{
Results.Add(resolved);
var references = GetAssemblyReferences(resolved);
foreach (var reference in references)
{
pendingAssemblies.Push(reference);
}
}
}
}
}
IEnumerable<AssemblyNameReference> GetAssemblyReferences(AssemblyEntry current) =>
current.Definition.Modules.SelectMany(m => m.AssemblyReferences);
AssemblyEntry Resolve(AssemblyNameReference current)
{
if (Assembly.Definition.Name.Name == current.Name)
{
return Assembly;
}
var referencedAssemblyCandidate = FindCandidate(current, Dependencies);
var bclAssemblyCandidate = FindCandidate(current, Bcl);
// Resolution logic. For right now, we will prefer the mono BCL version of a given
// assembly if there is a candidate assembly and an equivalent mono assembly.
if (bclAssemblyCandidate != null)
{
return bclAssemblyCandidate;
}
return referencedAssemblyCandidate;
}
AssemblyEntry FindCandidate(AssemblyNameReference current, AssemblyEntry[] candidates)
{
// Do simple name match. Assume no duplicates.
foreach (var candidate in candidates)
{
if (current.Name == candidate.Definition.Name.Name)
{
return candidate;
}
}
return null;
}
}
}
[DebuggerDisplay("{ToString(),nq}")]
public class AssemblyEntry
{
public AssemblyEntry(string path, AssemblyDefinition definition)
{
Path = path;
Definition = definition;
}
public string Path { get; set; }
public AssemblyDefinition Definition { get; set; }
public override string ToString() => Definition.FullName;
}
}
}

View File

@ -1,25 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
<TargetFrameworks>$(DefaultNetCoreTargetFramework);net46</TargetFrameworks>
<TargetName>Microsoft.AspNetCore.Blazor.Build.Tasks</TargetName>
<AssemblyName>Microsoft.AspNetCore.Blazor.Build</AssemblyName>
<Description>Build mechanism for ASP.NET Core Blazor applications.</Description>
<OutputType>Exe</OutputType>
<IsShippingPackage>false</IsShippingPackage>
<HasReferenceAssembly>false</HasReferenceAssembly>
<GenerateDependencyFile>false</GenerateDependencyFile>
</PropertyGroup>
<!-- Pack settings -->
<PropertyGroup>
<!-- Producing this package requires building with NodeJS enabled. -->
<IsPackable Condition="'$(BuildNodeJS)' == 'false'">false</IsPackable>
<GenerateNuspecDependsOn>$(GenerateNuspecDependsOn);Publish</GenerateNuspecDependsOn>
<NoPackageAnalysis>true</NoPackageAnalysis>
<NuspecFile>Microsoft.AspNetCore.Blazor.Build.nuspec</NuspecFile>
</PropertyGroup>
<ItemGroup>
<NuspecProperty Include="configuration=$(Configuration)" />
<NuspecProperty Include="publishDir=$(PublishDir)" />
<NuspecProperty Include="taskskDir=$(OutputPath)tools" />
<NuspecProperty Include="componentsversion=$(ComponentsPackageVersion)" />
<NuspecProperty Include="razorversion=$(MicrosoftAspNetCoreRazorDesignPackageVersion)" />
<NuspecProperty Include="blazormonoversion=$(MicrosoftAspNetCoreBlazorMonoPackageVersion)" />
@ -27,16 +28,45 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Condition="'$(BuildNodeJS)' != 'false' and '$(BuildingInsideVisualStudio)' != 'true'" Include="$(RepoRoot)src\Components\Web.JS\Microsoft.AspNetCore.Components.Web.JS.npmproj" ReferenceOutputAssembly="false" />
<Reference Include="Microsoft.AspNetCore.Components" />
<Reference Include="Microsoft.Extensions.CommandLineUtils.Sources" />
<Reference Include="Microsoft.Extensions.FileProviders.Composite" />
<Reference Include="Microsoft.Extensions.FileProviders.Physical" />
<Reference Include="Mono.Cecil" />
<Reference Include="System.CodeDom" />
<!-- Add a project dependency without reference output assemblies to enforce build order -->
<!-- Applying workaround for https://github.com/microsoft/msbuild/issues/2661 and https://github.com/dotnet/sdk/issues/952 -->
<ProjectReference
Include="$(RepoRoot)src\Components\Web.JS\Microsoft.AspNetCore.Components.Web.JS.npmproj"
ReferenceOutputAssemblies="false"
SkipGetTargetFrameworkProperties="true"
UndefineProperties="TargetFramework"
Private="false"
Condition="'$(BuildNodeJS)' != 'false' and '$(BuildingInsideVisualStudio)' != 'true'" />
<Reference Include="Microsoft.Build.Framework" ExcludeAssets="Runtime" />
<Reference Include="Microsoft.Build.Utilities.Core" ExcludeAssets="Runtime" />
<Reference Include="System.Reflection.Metadata" Condition="'$(TargetFramework)' == 'net46'" />
</ItemGroup>
<Target Name="CopyBuildTask" BeforeTargets="Build" Condition="'$(DotNetBuildFromSource)' != 'true' AND '$(IsInnerBuild)' != 'true'">
<!--
The task produced by this project is referenced within this solution. When building, Visual Studio will lock up the assembly.
Any attempts to overwrite the binary with a newer version will fail. This is particularly grating if a developer "Rebuilds" the project
after an initial build since that would always attempt to overwrite the tasks dll
This target attempts to make this solution more usable at the cost of a more onerous inner-loop build of the Blazor.Build tasks.
We'll copy the tasks to a location other that than the build output and use that in the Blazor.Build.targets. In the most common
case where these tasks aren't being worked on, everything should work great. However, if you're attemping to modify these tasks,
you will need to manaully stop MSBuild.exe processes
-->
<ItemGroup>
<Compile Include="..\..\..\Shared\src\JsonSerializerOptionsProvider.cs" />
<_NetCoreFilesToCopy Include="$(OutputPath)$(DefaultNetCoreTargetFramework)\*" TargetPath="netcoreapp\" />
<_DesktopFilesToCopy Include="$(OutputPath)net46\*" TargetPath="netfx\" />
<_AllFilesToCopy Include="@(_NetCoreFilesToCopy);@(_DesktopFilesToCopy)" />
</ItemGroup>
<Error Text="No files found in $(OutputPath)$(DefaultNetCoreTargetFramework)" Condition="@(_NetCoreFilesToCopy->Count()) == 0" />
<Error Text="No files found in $(OutputPath)net46" Condition="@(_DesktopFilesToCopy->Count()) == 0" />
<Copy SourceFiles="@(_AllFilesToCopy)" DestinationFiles="@(_AllFilesToCopy->'$(OutputPath)tools\%(TargetPath)%(FileName)%(Extension)')" SkipUnchangedFiles="true" Retries="1" ContinueOnError="true">
<Output TaskParameter="CopiedFiles" ItemName="FileWrites" />
</Copy>
</Target>
</Project>

View File

@ -11,7 +11,7 @@
<file src="$PackageThirdPartyNoticesFile$" target=".\THIRD-PARTY-NOTICES.txt" />
<file src="build\**" target="build" />
<file src="targets\**" target="targets" />
<file src="$publishdir$**\*" target="tools/" />
<file src="..\..\..\Web.JS\dist\$configuration$\blazor.*.js" target="tools/blazor" />
<file src="$taskskDir$\**" target="tools/" />
<file src="..\..\..\Web.JS\dist\$configuration$\blazor.webassembly.js" target="tools/blazor" />
</files>
</package>

View File

@ -0,0 +1,25 @@
<Project>
<!--
Importing this file is equivalent to having:
<PackageDependency Include="Microsoft.AspNetCore.Blazor.Build" />
... except it's much more convenient when working in this repo, because it consumes the
Blazor.Build targets/exe directly without needing this project to be packed into a .nupkg.
This is only intended for use by other projects in this repo.
-->
<PropertyGroup>
<ComponentsRoot Condition="'$(ComponentsRoot)'==''">$(MSBuildThisFileDirectory)..\..\..\</ComponentsRoot>
<BlazorJsPath>$(ComponentsRoot)Web.JS\dist\$(Configuration)\blazor.webassembly.js</BlazorJsPath>
<BlazorJsMapPath>$(ComponentsRoot)Web.JS\dist\$(Configuration)\blazor.webassembly.js.map</BlazorJsMapPath>
<BlazorToolsDir>$(MSBuildThisFileDirectory)bin\$(Configuration)\tools\</BlazorToolsDir>
</PropertyGroup>
<Target Name="CheckBlazorJSFiles" BeforeTargets="Build">
<Error Text="blazor.webassembly.js file could not be found at $(BlazorJsPath)" Condition="!Exists($(BlazorJsPath))" />
</Target>
<Import Project="$(MSBuildThisFileDirectory)targets/All.props" />
<Import Project="$(MSBuildThisFileDirectory)targets/All.targets" />
</Project>

View File

@ -1,21 +1,6 @@
<Project>
<!--
Importing this file is equivalent to having:
<PackageDependency Include="Microsoft.AspNetCore.Blazor.Build" />
... except it's much more convenient when working in this repo, because it consumes the
Blazor.Build targets/exe directly without needing this project to be packed into a .nupkg.
This is only intended for use by other projects in this repo.
-->
<PropertyGroup>
<BlazorBuildReferenceFromSource>true</BlazorBuildReferenceFromSource>
<BlazorJsPath>$(RepoRoot)src\Components\Web.JS\dist\$(Configuration)\blazor.*.js.*</BlazorJsPath>
</PropertyGroup>
<Import Project="$(MSBuildThisFileDirectory)targets/All.props" />
<Import Project="$(MSBuildThisFileDirectory)targets/All.targets" />
<Import Project="ReferenceBlazorBuildFromSource.props" />
<!--
Debugging support using blazor-devserver serve.
@ -35,6 +20,14 @@
<Reference Include="Microsoft.AspNetCore.Blazor.Mono" />
</ItemGroup>
<Target Name="_BuildBlazorBuildProject" BeforeTargets="ResolveProjectReferences">
<!--
The Blazor.Build project cross-compiles and we need the output of all TFMs to be available in the build output.
We can't represent this in any good way using ProjectReference elements, but we can try and build it instead.
-->
<MSBuild Projects="$(MSBuildThisFileDirectory)Microsoft.AspNetCore.Blazor.Build.csproj" />
</Target>
<!-- This is used as a P2P when building the repo. Normal Blazor projects will get this as a reference through the Blazor.Build package -->
<ItemGroup>
<!-- Ensures these projects are built before the consuming project, but without
@ -42,9 +35,8 @@
given that the packed version of this project wouldn't add a .dll reference) -->
<ProjectReference Include="$(MSBuildThisFileDirectory)Microsoft.AspNetCore.Blazor.Build.csproj">
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
<!-- Optimization. Do not require framework compatibility between these projects. -->
<SkipGetTargetFrameworkProperties>true</SkipGetTargetFrameworkProperties>
<UndefineProperties>TargetFramework</UndefineProperties>
<Properties>TargetFramework=$(DefaultNetCoreTargetFramework)</Properties>
</ProjectReference>
<ProjectReference Include="$(MSBuildThisFileDirectory)..\..\DevServer\src\Microsoft.AspNetCore.Blazor.DevServer.csproj">
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>

View File

@ -0,0 +1,56 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml;
using System.Xml.Linq;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
namespace Microsoft.AspNetCore.Blazor.Build
{
// Based on https://github.com/mono/linker/blob/3b329b9481e300bcf4fb88a2eebf8cb5ef8b323b/src/ILLink.Tasks/CreateRootDescriptorFile.cs
public class BlazorCreateRootDescriptorFile : Task
{
[Required]
public ITaskItem[] AssemblyNames { get; set; }
[Required]
public ITaskItem RootDescriptorFilePath { get; set; }
public override bool Execute()
{
using var fileStream = File.Create(RootDescriptorFilePath.ItemSpec);
var assemblyNames = AssemblyNames.Select(a => a.ItemSpec);
WriteRootDescriptor(fileStream, assemblyNames);
return true;
}
internal static void WriteRootDescriptor(Stream stream, IEnumerable<string> assemblyNames)
{
var roots = new XElement("linker");
foreach (var assemblyName in assemblyNames)
{
roots.Add(new XElement("assembly",
new XAttribute("fullname", assemblyName),
new XElement("type",
new XAttribute("fullname", "*"),
new XAttribute("required", "true"))));
}
var xmlWriterSettings = new XmlWriterSettings
{
Indent = true,
OmitXmlDeclaration = true
};
using var writer = XmlWriter.Create(stream, xmlWriterSettings);
var xDocument = new XDocument(roots);
xDocument.Save(writer);
}
}
}

View File

@ -0,0 +1,194 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
namespace Microsoft.AspNetCore.Blazor.Build.Tasks
{
// Based on https://github.com/mono/linker/blob/3b329b9481e300bcf4fb88a2eebf8cb5ef8b323b/src/ILLink.Tasks/LinkTask.cs
public class BlazorILLink : ToolTask
{
private const string DotNetHostPathEnvironmentName = "DOTNET_HOST_PATH";
[Required]
public string ILLinkPath { get; set; }
[Required]
public ITaskItem[] AssemblyPaths { get; set; }
public ITaskItem[] ReferenceAssemblyPaths { get; set; }
[Required]
public ITaskItem[] RootAssemblyNames { get; set; }
[Required]
public ITaskItem OutputDirectory { get; set; }
public ITaskItem[] RootDescriptorFiles { get; set; }
public bool ClearInitLocals { get; set; }
public string ClearInitLocalsAssemblies { get; set; }
public string ExtraArgs { get; set; }
public bool DumpDependencies { get; set; }
private string _dotnetPath;
private string DotNetPath
{
get
{
if (!string.IsNullOrEmpty(_dotnetPath))
{
return _dotnetPath;
}
_dotnetPath = Environment.GetEnvironmentVariable(DotNetHostPathEnvironmentName);
if (string.IsNullOrEmpty(_dotnetPath))
{
throw new InvalidOperationException($"{DotNetHostPathEnvironmentName} is not set");
}
return _dotnetPath;
}
}
protected override MessageImportance StandardErrorLoggingImportance => MessageImportance.High;
protected override string ToolName => Path.GetFileName(DotNetPath);
protected override string GenerateFullPathToTool() => DotNetPath;
protected override string GenerateCommandLineCommands()
{
var args = new StringBuilder();
args.Append(Quote(ILLinkPath));
return args.ToString();
}
private static string Quote(string path)
{
return $"\"{path.TrimEnd('\\')}\"";
}
protected override string GenerateResponseFileCommands()
{
var args = new StringBuilder();
if (RootDescriptorFiles != null)
{
foreach (var rootFile in RootDescriptorFiles)
{
args.Append("-x ").AppendLine(Quote(rootFile.ItemSpec));
}
}
foreach (var assemblyItem in RootAssemblyNames)
{
args.Append("-a ").AppendLine(Quote(assemblyItem.ItemSpec));
}
var assemblyNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var assembly in AssemblyPaths)
{
var assemblyPath = assembly.ItemSpec;
var assemblyName = Path.GetFileNameWithoutExtension(assemblyPath);
// If there are multiple paths with the same assembly name, only use the first one.
if (!assemblyNames.Add(assemblyName))
{
continue;
}
args.Append("-reference ")
.AppendLine(Quote(assemblyPath));
var action = assembly.GetMetadata("action");
if ((action != null) && (action.Length > 0))
{
args.Append("-p ");
args.Append(action);
args.Append(" ").AppendLine(Quote(assemblyName));
}
}
if (ReferenceAssemblyPaths != null)
{
foreach (var assembly in ReferenceAssemblyPaths)
{
var assemblyPath = assembly.ItemSpec;
var assemblyName = Path.GetFileNameWithoutExtension(assemblyPath);
// Don't process references for which we already have
// implementation assemblies.
if (assemblyNames.Contains(assemblyName))
{
continue;
}
args.Append("-reference ").AppendLine(Quote(assemblyPath));
// Treat reference assemblies as "skip". Ideally we
// would not even look at the IL, but only use them to
// resolve surface area.
args.Append("-p skip ").AppendLine(Quote(assemblyName));
}
}
if (OutputDirectory != null)
{
args.Append("-out ").AppendLine(Quote(OutputDirectory.ItemSpec));
}
if (ClearInitLocals)
{
args.AppendLine("--enable-opt clearinitlocals");
if ((ClearInitLocalsAssemblies != null) && (ClearInitLocalsAssemblies.Length > 0))
{
args.Append("-m ClearInitLocalsAssemblies ");
args.AppendLine(ClearInitLocalsAssemblies);
}
}
if (ExtraArgs != null)
{
args.AppendLine(ExtraArgs);
}
if (DumpDependencies)
{
args.AppendLine("--dump-dependencies");
}
return args.ToString();
}
protected override bool HandleTaskExecutionErrors()
{
// Show a slightly better error than the standard ToolTask message that says "dotnet" failed.
Log.LogError($"ILLink failed with exit code {ExitCode}.");
return false;
}
protected override void LogEventsFromTextOutput(string singleLine, MessageImportance messageImportance)
{
if (!string.IsNullOrEmpty(singleLine) && singleLine.StartsWith("Unhandled exception.", StringComparison.Ordinal))
{
// The Mono linker currently prints out an entire stack trace when the linker fails.
// We want to show something actionable in the VS Error window.
Log.LogError(singleLine);
}
else
{
base.LogEventsFromTextOutput(singleLine, messageImportance);
}
}
}
}

View File

@ -0,0 +1,86 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Serialization.Json;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
namespace Microsoft.AspNetCore.Blazor.Build
{
public class GenerateBlazorBootJson : Task
{
[Required]
public string AssemblyPath { get; set; }
[Required]
public ITaskItem[] References { get; set; }
[Required]
public bool LinkerEnabled { get; set; }
[Required]
public string OutputPath { get; set; }
public override bool Execute()
{
var entryAssemblyName = AssemblyName.GetAssemblyName(AssemblyPath).Name;
var assemblies = References.Select(GetUriPath).OrderBy(c => c, StringComparer.Ordinal).ToArray();
using var fileStream = File.Create(OutputPath);
WriteBootJson(fileStream, entryAssemblyName, assemblies, LinkerEnabled);
return true;
static string GetUriPath(ITaskItem item)
{
var outputPath = item.GetMetadata("RelativeOutputPath");
if (string.IsNullOrEmpty(outputPath))
{
outputPath = Path.GetFileName(item.ItemSpec);
}
return outputPath.Replace('\\', '/');
}
}
internal static void WriteBootJson(Stream stream, string entryAssemblyName, string[] assemblies, bool linkerEnabled)
{
var data = new BootJsonData
{
entryAssembly = entryAssemblyName,
assemblies = assemblies,
linkerEnabled = linkerEnabled,
};
var serializer = new DataContractJsonSerializer(typeof(BootJsonData));
serializer.WriteObject(stream, data);
}
/// <summary>
/// Defines the structure of a Blazor boot JSON file
/// </summary>
#pragma warning disable IDE1006 // Naming Styles
public class BootJsonData
{
/// <summary>
/// Gets the name of the assembly with the application entry point
/// </summary>
public string entryAssembly { get; set; }
/// <summary>
/// Gets the closure of assemblies to be loaded by Blazor WASM. This includes the application entry assembly.
/// </summary>
public string[] assemblies { get; set; }
/// <summary>
/// Gets a value that determines if the linker is enabled.
/// </summary>
public bool linkerEnabled { get; set; }
}
#pragma warning restore IDE1006 // Naming Styles
}
}

View File

@ -0,0 +1,48 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.IO;
using System.Xml.Linq;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
namespace Microsoft.AspNetCore.Blazor.Build.Tasks
{
public class GenerateTypeGranularityLinkingConfig : Task
{
[Required]
public ITaskItem[] Assemblies { get; set; }
[Required]
public string OutputPath { get; set; }
public override bool Execute()
{
var linkerElement = new XElement("linker",
new XComment(" THIS IS A GENERATED FILE - DO NOT EDIT MANUALLY "));
foreach (var assembly in Assemblies)
{
var assemblyElement = CreateTypeGranularityConfig(assembly);
linkerElement.Add(assemblyElement);
}
using var fileStream = File.Open(OutputPath, FileMode.Create);
new XDocument(linkerElement).Save(fileStream);
return true;
}
private XElement CreateTypeGranularityConfig(ITaskItem assembly)
{
// We match all types in the assembly, and for each one, tell the linker to preserve all
// its members (preserve=all) but only if there's some reference to the type (required=false)
return new XElement("assembly",
new XAttribute("fullname", Path.GetFileNameWithoutExtension(assembly.ItemSpec)),
new XElement("type",
new XAttribute("fullname", "*"),
new XAttribute("preserve", "all"),
new XAttribute("required", "false")));
}
}
}

View File

@ -0,0 +1,203 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Reflection.Metadata;
using System.Reflection.PortableExecutable;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
namespace Microsoft.AspNetCore.Blazor.Build
{
public class ResolveBlazorRuntimeDependencies : Task
{
[Required]
public string EntryPoint { get; set; }
[Required]
public ITaskItem[] ApplicationDependencies { get; set; }
[Required]
public ITaskItem[] WebAssemblyBCLAssemblies { get; set; }
[Output]
public ITaskItem[] Dependencies { get; set; }
public override bool Execute()
{
var paths = ResolveRuntimeDependenciesCore(EntryPoint, ApplicationDependencies.Select(c => c.ItemSpec), WebAssemblyBCLAssemblies.Select(c => c.ItemSpec));
Dependencies = paths.Select(p => new TaskItem(p)).ToArray();
return true;
}
public static IEnumerable<string> ResolveRuntimeDependenciesCore(
string entryPoint,
IEnumerable<string> applicationDependencies,
IEnumerable<string> monoBclAssemblies)
{
var entryAssembly = new AssemblyEntry(entryPoint, GetAssemblyName(entryPoint));
var dependencies = CreateAssemblyLookup(applicationDependencies);
var bcl = CreateAssemblyLookup(monoBclAssemblies);
var assemblyResolutionContext = new AssemblyResolutionContext(
entryAssembly,
dependencies,
bcl);
assemblyResolutionContext.ResolveAssemblies();
var paths = assemblyResolutionContext.Results.Select(r => r.Path);
return paths.Concat(FindPdbs(paths));
static Dictionary<string, AssemblyEntry> CreateAssemblyLookup(IEnumerable<string> assemblyPaths)
{
var dictionary = new Dictionary<string, AssemblyEntry>(StringComparer.Ordinal);
foreach (var path in assemblyPaths)
{
var assemblyName = AssemblyName.GetAssemblyName(path).Name;
if (dictionary.TryGetValue(assemblyName, out var previous))
{
throw new InvalidOperationException($"Multiple assemblies found with the same assembly name '{assemblyName}':" +
Environment.NewLine + string.Join(Environment.NewLine, previous, path));
}
dictionary[assemblyName] = new AssemblyEntry(path, assemblyName);
}
return dictionary;
}
}
private static string GetAssemblyName(string assemblyPath)
{
return AssemblyName.GetAssemblyName(assemblyPath).Name;
}
private static IEnumerable<string> FindPdbs(IEnumerable<string> dllPaths)
{
return dllPaths
.Select(path => Path.ChangeExtension(path, "pdb"))
.Where(path => File.Exists(path));
}
public class AssemblyResolutionContext
{
public AssemblyResolutionContext(
AssemblyEntry entryAssembly,
Dictionary<string, AssemblyEntry> dependencies,
Dictionary<string, AssemblyEntry> bcl)
{
EntryAssembly = entryAssembly;
Dependencies = dependencies;
Bcl = bcl;
}
public AssemblyEntry EntryAssembly { get; }
public Dictionary<string, AssemblyEntry> Dependencies { get; }
public Dictionary<string, AssemblyEntry> Bcl { get; }
public IList<AssemblyEntry> Results { get; } = new List<AssemblyEntry>();
internal void ResolveAssemblies()
{
var visitedAssemblies = new HashSet<string>();
var pendingAssemblies = new Stack<string>();
pendingAssemblies.Push(EntryAssembly.Name);
ResolveAssembliesCore();
void ResolveAssembliesCore()
{
while (pendingAssemblies.Count > 0)
{
var current = pendingAssemblies.Pop();
if (visitedAssemblies.Add(current))
{
// Not all references will be resolvable within the Mono BCL.
// Skipping unresolved assemblies here is equivalent to passing "--skip-unresolved true" to the Mono linker.
if (Resolve(current) is AssemblyEntry resolved)
{
Results.Add(resolved);
var references = GetAssemblyReferences(resolved.Path);
foreach (var reference in references)
{
pendingAssemblies.Push(reference);
}
}
}
}
}
AssemblyEntry? Resolve(string assemblyName)
{
if (EntryAssembly.Name == assemblyName)
{
return EntryAssembly;
}
// Resolution logic. For right now, we will prefer the mono BCL version of a given
// assembly if there is a candidate assembly and an equivalent mono assembly.
if (Bcl.TryGetValue(assemblyName, out var assembly) ||
Dependencies.TryGetValue(assemblyName, out assembly))
{
return assembly;
}
return null;
}
static IReadOnlyList<string> GetAssemblyReferences(string assemblyPath)
{
try
{
using var peReader = new PEReader(File.OpenRead(assemblyPath));
if (!peReader.HasMetadata)
{
return Array.Empty<string>(); // not a managed assembly
}
var metadataReader = peReader.GetMetadataReader();
var references = new List<string>();
foreach (var handle in metadataReader.AssemblyReferences)
{
var reference = metadataReader.GetAssemblyReference(handle);
var referenceName = metadataReader.GetString(reference.Name);
references.Add(referenceName);
}
return references;
}
catch (BadImageFormatException)
{
// not a PE file, or invalid metadata
}
return Array.Empty<string>(); // not a managed assembly
}
}
}
[DebuggerDisplay("{ToString(),nq}")]
public readonly struct AssemblyEntry
{
public AssemblyEntry(string path, string name)
{
Path = path;
Name = name;
}
public string Path { get; }
public string Name { get; }
public override string ToString() => Name;
}
}
}

View File

@ -4,12 +4,6 @@
<PropertyGroup>
<DefaultWebContentItemExcludes>$(DefaultWebContentItemExcludes);wwwroot\**</DefaultWebContentItemExcludes>
<!-- By default, enable auto rebuilds for debug builds. Note that the server will not enable it in production environments regardless. -->
<BlazorRebuildOnFileChange Condition="'$(Configuration)' == 'Debug' AND '$(BlazorRebuildOnFileChange)' == ''">true</BlazorRebuildOnFileChange>
<!-- By default, enable debugging for debug builds. -->
<BlazorEnableDebugging Condition="'$(Configuration)' == 'Debug' AND '$(BlazorEnableDebugging)' == ''">true</BlazorEnableDebugging>
<!-- When using IISExpress with a standalone app, there's no point restarting IISExpress after build. It slows things unnecessarily and breaks in-flight HTTP requests. -->
<NoRestartServerOnBuild>true</NoRestartServerOnBuild>
</PropertyGroup>

View File

@ -6,41 +6,44 @@
</PropertyGroup>
<PropertyGroup>
<BlazorToolsDir Condition="'$(BlazorToolsDir)' == ''">$(MSBuildThisFileDirectory)../tools/</BlazorToolsDir>
<BlazorBuildExe>dotnet &quot;$(BlazorToolsDir)Microsoft.AspNetCore.Blazor.Build.dll&quot;</BlazorBuildExe>
<BlazorToolsDir Condition="'$(BlazorToolsDir)' == ''">$(MSBuildThisFileDirectory)..\tools\</BlazorToolsDir>
<_BlazorTasksTFM Condition=" '$(MSBuildRuntimeType)' == 'Core'">netcoreapp</_BlazorTasksTFM>
<_BlazorTasksTFM Condition=" '$(_BlazorTasksTFM)' == ''">netfx</_BlazorTasksTFM>
<BlazorTasksPath>$(BlazorToolsDir)$(_BlazorTasksTFM)\Microsoft.AspNetCore.Blazor.Build.Tasks.dll</BlazorTasksPath>
<!-- The Blazor build code can only find your referenced assemblies if they are in the output directory -->
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<!-- By default, enable debugging for debug builds. -->
<BlazorEnableDebugging Condition="'$(Configuration)' == 'Debug' AND '$(BlazorEnableDebugging)' == ''">true</BlazorEnableDebugging>
</PropertyGroup>
<Import Project="Blazor.MonoRuntime.targets" />
<Import Project="Publish.targets" />
<Import Project="StaticWebAssets.targets" />
<Target Name="GenerateBlazorMetadataFile" BeforeTargets="GetCopyToOutputDirectoryItems">
<Target Name="GenerateBlazorMetadataFile"
BeforeTargets="GetCopyToOutputDirectoryItems">
<PropertyGroup>
<BlazorMetadataFileName>$(AssemblyName).blazor.config</BlazorMetadataFileName>
<BlazorMetadataFilePath>$(TargetDir)$(BlazorMetadataFileName)</BlazorMetadataFilePath>
</PropertyGroup>
<WriteLinesToFile File="$(BlazorMetadataFilePath)" Lines="$(MSBuildProjectFullPath)" Overwrite="true" Encoding="Unicode"/>
<WriteLinesToFile File="$(BlazorMetadataFilePath)" Lines="$(OutDir)$(AssemblyName).dll" Overwrite="false" Encoding="Unicode"/>
<WriteLinesToFile File="$(BlazorMetadataFilePath)" Condition="'$(BlazorRebuildOnFileChange)'=='true'" Lines="autorebuild:true" Overwrite="false" Encoding="Unicode"/>
<WriteLinesToFile File="$(BlazorMetadataFilePath)" Condition="'$(BlazorEnableDebugging)'=='true'" Lines="debug:true" Overwrite="false" Encoding="Unicode"/>
<ItemGroup>
<_BlazorConfigContent Include="$(MSBuildProjectFullPath)" />
<_BlazorConfigContent Include="$(TargetPath)" />
<_BlazorConfigContent Include="debug:true" Condition="'$(BlazorEnableDebugging)'=='true'" />
</ItemGroup>
<WriteLinesToFile
File="$(BlazorMetadataFilePath)"
Lines="@(_BlazorConfigContent)"
Overwrite="true"
WriteOnlyWhenDifferent="True" />
<ItemGroup>
<ContentWithTargetPath Include="$(BlazorMetadataFilePath)" TargetPath="$(BlazorMetadataFileName)" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Target>
<PropertyGroup>
<GetCurrentProjectStaticWebAssetsDependsOn>
$(GetCurrentProjectStaticWebAssetsDependsOn);
_ClearCurrentStaticWebAssetsForReferenceDiscovery
</GetCurrentProjectStaticWebAssetsDependsOn>
</PropertyGroup>
<Target Name="_ClearCurrentStaticWebAssetsForReferenceDiscovery">
<ItemGroup>
<StaticWebAsset Remove="@(StaticWebAsset)" Condition="'%(SourceType)' == ''" />
</ItemGroup>
</Target>
</Project>

View File

@ -1,22 +1,20 @@
<Project>
<PropertyGroup Condition="'$(BlazorBuildReferenceFromSource)'==''">
<BlazorJsPath>$(MSBuildThisFileDirectory)../tools/blazor/blazor.*.js</BlazorJsPath>
<PropertyGroup>
<BlazorJsPath Condition="'$(BlazorJsPath)' == ''">$(MSBuildThisFileDirectory)..\tools\blazor\blazor.webassembly.js</BlazorJsPath>
</PropertyGroup>
<PropertyGroup Label="Blazor build outputs">
<MonoLinkerI18NAssemblies>none</MonoLinkerI18NAssemblies> <!-- See Mono linker docs - allows comma-separated values from: none,all,cjk,mideast,other,rare,west -->
<AdditionalMonoLinkerOptions>--disable-opt unreachablebodies --verbose --strip-security true --exclude-feature com --exclude-feature sre -v false -c link -u link -b true</AdditionalMonoLinkerOptions>
<BaseBlazorDistPath>dist/</BaseBlazorDistPath>
<BaseBlazorPackageContentOutputPath>$(BaseBlazorDistPath)_content/</BaseBlazorPackageContentOutputPath>
<BaseBlazorRuntimeOutputPath>$(BaseBlazorDistPath)_framework/</BaseBlazorRuntimeOutputPath>
<BaseBlazorRuntimeBinOutputPath>$(BaseBlazorRuntimeOutputPath)_bin/</BaseBlazorRuntimeBinOutputPath>
<BaseBlazorRuntimeWasmOutputPath>$(BaseBlazorRuntimeOutputPath)wasm/</BaseBlazorRuntimeWasmOutputPath>
<BaseBlazorJsOutputPath>$(BaseBlazorRuntimeOutputPath)</BaseBlazorJsOutputPath>
<BaseBlazorIntermediateOutputPath>blazor/</BaseBlazorIntermediateOutputPath>
<BlazorWebRootName>wwwroot/</BlazorWebRootName>
<AdditionalMonoLinkerOptions>--disable-opt unreachablebodies --verbose --strip-security true --exclude-feature com -v false -c link -u link -b true</AdditionalMonoLinkerOptions>
<BaseBlazorDistPath>dist\</BaseBlazorDistPath>
<BaseBlazorPackageContentOutputPath>$(BaseBlazorDistPath)_content\</BaseBlazorPackageContentOutputPath>
<BaseBlazorRuntimeOutputPath>$(BaseBlazorDistPath)_framework\</BaseBlazorRuntimeOutputPath>
<BlazorRuntimeBinOutputPath>$(BaseBlazorRuntimeOutputPath)_bin\</BlazorRuntimeBinOutputPath>
<BlazorRuntimeWasmOutputPath>$(BaseBlazorRuntimeOutputPath)wasm\</BlazorRuntimeWasmOutputPath>
<BlazorWebRootName>wwwroot\</BlazorWebRootName>
<BlazorBootJsonName>blazor.boot.json</BlazorBootJsonName>
<BlazorBootJsonOutputPath>$(BaseBlazorRuntimeOutputPath)$(BlazorBootJsonName)</BlazorBootJsonOutputPath>
<_BlazorBuiltInBclLinkerDescriptor>$(MSBuildThisFileDirectory)BuiltInBclLinkerDescriptor.xml</_BlazorBuiltInBclLinkerDescriptor>
</PropertyGroup>
</Project>

View File

@ -1,653 +1,336 @@
<Project>
<PropertyGroup>
<BlazorLinkOnBuild Condition="$(BlazorLinkOnBuild) == ''">true</BlazorLinkOnBuild>
</PropertyGroup>
<PropertyGroup>
<!-- Stop-gap until we can migrate Blazor.Mono package to use better naming convention -->
<DotNetWebAssemblyBCLPath Condition="'$(DotNetWebAssemblyBCLPath)' == '' AND '$(MonoBaseClassLibraryPath)' != ''">$(MonoBaseClassLibraryPath)</DotNetWebAssemblyBCLPath>
<DotNetWebAssemblyBCLFacadesPath Condition="'$(DotNetWebAssemblyBCLFacadesPath)' == '' AND '$(MonoBaseClassLibraryFacadesPath)' != ''">$(MonoBaseClassLibraryFacadesPath)</DotNetWebAssemblyBCLFacadesPath>
<DotNetWebAssemblyRuntimePath Condition="'$(DotNetWebAssemblyRuntimePath)' == '' AND '$(MonoWasmRuntimePath)' != ''">$(MonoWasmRuntimePath)</DotNetWebAssemblyRuntimePath>
<DotNetWebAssemblyFrameworkPath Condition="'$(DotNetWebAssemblyFrameworkPath)' == '' AND '$(MonoWasmFrameworkPath)' != ''">$(MonoWasmFrameworkPath)</DotNetWebAssemblyFrameworkPath>
</PropertyGroup>
<PropertyGroup Condition="'$(DotNetWebAssemblyArtifactsRoot)' != ''">
<!-- Compute paths given a path to DotNet WASM artifacts. This is meant to make it easy to test WASM builds -->
<DotNetWebAssemblyBCLPath>$(DotNetWebAssemblyArtifactsRoot)\wasm-bcl\wasm\</DotNetWebAssemblyBCLPath>
<DotNetWebAssemblyBCLFacadesPath>$(DotNetWebAssemblyBCLPath)\Facades\</DotNetWebAssemblyBCLFacadesPath>
<DotNetWebAssemblyRuntimePath>$(DotNetWebAssemblyArtifactsRoot)\builds\debug\</DotNetWebAssemblyRuntimePath>
<DotNetWebAssemblyFrameworkPath>$(DotNetWebAssemblyArtifactsRoot)\framework\</DotNetWebAssemblyFrameworkPath>
</PropertyGroup>
<Target
Name="_BlazorCopyFilesToOutputDirectory"
DependsOnTargets="PrepareBlazorOutputs"
Inputs="@(BlazorItemOutput)"
Outputs="@(BlazorItemOutput->'%(TargetOutputPath)')"
AfterTargets="CopyFilesToOutputDirectory"
Condition="'$(OutputType.ToLowerInvariant())'=='exe'">
<!-- Copy the blazor output files -->
<Copy
SourceFiles="@(BlazorItemOutput)"
DestinationFiles="@(BlazorItemOutput->'%(TargetOutputPath)')"
SourceFiles="@(BlazorOutputWithTargetPath)"
DestinationFiles="@(BlazorOutputWithTargetPath->'$(TargetDir)%(TargetOutputPath)')"
SkipUnchangedFiles="$(SkipCopyUnchangedFiles)"
OverwriteReadOnlyFiles="$(OverwriteReadOnlyFiles)"
Retries="$(CopyRetryCount)"
RetryDelayMilliseconds="$(CopyRetryDelayMilliseconds)"
UseHardlinksIfPossible="$(CreateHardLinksForCopyFilesToOutputDirectoryIfPossible)"
UseSymboliclinksIfPossible="$(CreateSymbolicLinksForCopyFilesToOutputDirectoryIfPossible)"
Condition="'@(BlazorItemOutput)' != '' and '$(CopyBuildOutputToOutputDirectory)' == 'true' and '$(SkipCopyBuildProduct)' != 'true'">
Condition="'@(BlazorOutputWithTargetPath)' != '' and '$(CopyBuildOutputToOutputDirectory)' == 'true' and '$(SkipCopyBuildProduct)' != 'true'">
</Copy>
<ItemGroup>
<FileWrites Include="@(BlazorItemOutput->'%(TargetOutputPath)')" />
<FileWrites Include="@(BlazorOutputWithTargetPath->'$(TargetDir)%(TargetOutputPath)')" />
</ItemGroup>
</Target>
<Target Name="_BlazorTrackResolveReferencesDidRun" AfterTargets="ResolveReferences">
<PropertyGroup>
<!-- So we know we can trust @(ReferenceCopyLocalPaths) later -->
<_BlazorResolveReferencesDidRun>true</_BlazorResolveReferencesDidRun>
</PropertyGroup>
</Target>
<Target Name="_BlazorBuildReport"
AfterTargets="_BlazorCopyFilesToOutputDirectory">
<ItemGroup>
<_BlazorStatisticsOutput Include="@(BlazorItemOutput->'%(TargetOutputPath)')" />
<_BlazorStatisticsOutput Include="@(BlazorOutputWithTargetPath->'%(TargetOutputPath)')" />
</ItemGroup>
<PropertyGroup>
<_BlazorStatisticsReportImportance Condition="'$(BlazorOutputStatistics)' == ''">normal</_BlazorStatisticsReportImportance>
<_BlazorStatisticsReportImportance Condition="'$(BlazorOutputStatistics)' != ''">high</_BlazorStatisticsReportImportance>
</PropertyGroup>
<Message Importance="high" Text="Blazor Build result -> @(_BlazorStatisticsOutput->Distinct()->Count()) files in $(TargetDir)dist" />
<Message Importance="$(_BlazorStatisticsReportImportance)" Text="%(_BlazorStatisticsOutput.Identity)" />
<Message Importance="high" Text="$(TargetName) (Blazor output) -> $(TargetDir)dist" />
</Target>
<!-- Preparing blazor files for output:
PrepareBlazorOutputs
_PrepareBlazorOutputConfiguration
_DefineBlazorCommonInputs
_BlazorResolveOutputBinaries
When link on build:
_GenerateLinkerDescriptor
_CollectBlazorLinkerDescriptors
_LinkBlazorApplication
_CollectLinkerOutputs
When don't link on build:
_CollectResolvedAssemblies
_ResolveBlazorApplicationAssemblies
_ReadResolvedBlazorApplicationAssemblies
_IntermediateCopyBlazorApplicationAssemblies
_TouchBlazorApplicationAssemblies
_GenerateBlazorBootJson
_BlazorCopyFilesToOutputDirectory
<Target
Name="PrepareBlazorOutputs"
DependsOnTargets="_ResolveBlazorInputs;_ResolveBlazorOutputs;_GenerateBlazorBootJson">
The process for doing builds goes as follows:
Produce a hash file with the Hash SDK task and write that hash to a marker file.
Produce a marker file that saves whether we are linking or not in this build so that we can take that as
input in future builds and do the correct thing for incremental builds.
We only produce marker files when the input changes, if the input doesn't change the marker stays the
same.
<ItemGroup>
<MonoWasmFile Include="$(DotNetWebAssemblyRuntimePath)*" />
<BlazorJSFile Include="$(BlazorJSPath)" />
<BlazorJSFile Include="$(BlazorJSMapPath)" Condition="Exists('$(BlazorJSMapPath)')" />
If we are linking on this build the process is as follows:
1) We determine if there are linker descriptors available, if not generate one.
2) Collect the list of linker descriptors and create a marker for the linker if it doesn't exist or changed
from a previous build.
3) Run the linker in case the linker inputs marker is newer than the linker result file.
4) Collect the outputs from the linker.
If we are not linking in this build the process is as follows:
1) Resolve the assemblies for the application only if the inputs marker is newer than the resolved assemblies
result file.
2) Read the result file with the resolved assemblies.
3) Copy the resolved assemblies to an intermediate folder.
4) In case we are switching from linking to not linking, touch the files in the intermediate folder to ensure
that updated versions of the files get copied to the output folder.
Once the binary outputs are resolved:
1) Create a marker file with the resolved assemblies and the boot json data as inputs.
2) If the marker file is newer than the boot json in the output folder, regenerate the
boot json
Once all the outputs are resolved (static content + binary outputs + boot json)
Copy all the files to the output folder.
-->
<PropertyGroup>
<PrepareBlazorOutputs>
_PrepareBlazorOutputConfiguration;
_DefineBlazorCommonInputs;
_BlazorResolveOutputBinaries;
_GenerateBlazorBootJson;
</PrepareBlazorOutputs>
</PropertyGroup>
<Target Name="PrepareBlazorOutputs" DependsOnTargets="$(PrepareBlazorOutputs)" />
<!--
Prepare blazor outputs preamble:
* Creates updated marker files (if necessary) for incremental builds.
* Computes intermediate and final output paths.
* Computes the list of static items to copy to the output folder.
-->
<Target Name="_PrepareBlazorOutputConfiguration">
<!--
This task produces all the "final" paths for all the files we need to produce the final output.
The final folder is something like bin/<<Configuration>>/<<TargetFramework>>/dist
/_framework/_bin <- This will contain either the BCL + app assemblies or the result of linking the app.
/_framework/wasm <- This will contain the wsm runtime copied from the nuget package.
/_framework/blazor.js <- This is the blazor.js file copied from the nuget package.
/_framework/blazor.boot.json <- This is the boot json file
This task also defines some intermediate paths that we will use:
/obj/<<configuration>>/<<targetframework>>/blazor/blazor/linker <- This will be used to create the output from the linker.
/obj/<<configuration>>/<<targetframework>>/blazor/blazor/linked.assemblies.txt <- This will be used to save the output files from
the linker and use that as marker to identify whether or not we need to run the linker.
/obj/<<configuration>>/<<targetframework>>/blazor/blazor/linker.descriptor.xml <- This will be used to generate an XML descriptor
for the mono linker.
/obj/<<configuration>>/<<targetframework>>/blazor/inputs.basic.cache <- This is the marker file to track the inputs common
inputs to the output generation process.
/obj/<<configuration>>/<<targetframework>>/blazor/inputs.copylocal.txt <- Paths to all the copy-local referenced assemblies found
during the build process (i.e., the @(ReferenceCopyLocalPaths) values). We need this because when publishing, the build doesn't
necessarily also run so this is the only way we know which assemblies to include in linking/resolveassemblies.
/obj/<<configuration>>/<<targetframework>>/blazor/inputs.linkerswitch.cache <- This is the marker file to track the
switch from linking to not linking and viceversa.
/obj/<<configuration>>/<<targetframework>>/blazor/inputs.linker.cache <- This is the marker file to track the inputs
to the linker.
/obj/<<configuration>>/<<targetframework>>/blazor/resolvedassemblies/ <- This will be used to store the resolved assemblies
before copying them to the output when linking is not enabled.
/obj/<<configuration>>/<<targetframework>>/blazor/resolved.assemblies.txt <- This keeps track of all the resolved assemblies.
/obj/<<configuration>>/<<targetframework>>/blazor/blazor.boot.json <- The generated boot json file
/obj/<<configuration>>/<<targetframework>>/blazor/inputs.bootjson.cache <- The marker file that track whether boot json needs to
be regenerated.
-->
<PropertyGroup Label="Build properties">
<_BlazorShouldLinkApplicationAssemblies Condition="$(BlazorLinkOnBuild) == 'false'"></_BlazorShouldLinkApplicationAssemblies>
<_BlazorShouldLinkApplicationAssemblies Condition="$(BlazorLinkOnBuild) == 'true'">true</_BlazorShouldLinkApplicationAssemblies>
<_BlazorBuiltInBclLinkerDescriptor>$(MSBuildThisFileDirectory)BuiltInBclLinkerDescriptor.xml</_BlazorBuiltInBclLinkerDescriptor>
</PropertyGroup>
<ItemGroup Label="Static content to copy to the output folder">
<MonoWasmFile Include="$(MonoWasmRuntimePath)**/*.*" />
<BlazorJsFile Include="$(BlazorJsPath)" />
<BlazorItemOutput Include="@(MonoWasmFile)">
<TargetOutputPath>$(TargetDir)$(BaseBlazorRuntimeWasmOutputPath)%(FileName)%(Extension)</TargetOutputPath>
<Type>WebAssembly</Type>
<IsStatic>true</IsStatic>
</BlazorItemOutput>
<BlazorItemOutput Include="@(BlazorJsFile)">
<TargetOutputPath>$(TargetDir)$(BaseBlazorJsOutputPath)%(FileName)%(Extension)</TargetOutputPath>
<Type>BlazorRuntime</Type>
<IsStatic>true</IsStatic>
</BlazorItemOutput>
<BlazorOutputWithTargetPath Include="@(MonoWasmFile)">
<TargetOutputPath>$(BlazorRuntimeWasmOutputPath)%(FileName)%(Extension)</TargetOutputPath>
</BlazorOutputWithTargetPath>
<BlazorOutputWithTargetPath Include="@(BlazorJSFile)">
<TargetOutputPath>$(BaseBlazorRuntimeOutputPath)%(FileName)%(Extension)</TargetOutputPath>
</BlazorOutputWithTargetPath>
</ItemGroup>
<Error Condition="'@(BlazorJsFile->Count())' == '0'" Text="No JS files found in '$(BlazorJsPath)'" />
<ItemGroup Label="Static content supplied by NuGet packages">
<_BlazorPackageContentOutput Include="@(BlazorPackageContentFile)" Condition="%(SourcePackage) != ''">
<TargetOutputPath>$(TargetDir)$(BaseBlazorPackageContentOutputPath)%(SourcePackage)\%(RecursiveDir)\%(Filename)%(Extension)</TargetOutputPath>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<TargetOutputPath>$(BaseBlazorPackageContentOutputPath)%(SourcePackage)\%(RecursiveDir)\%(Filename)%(Extension)</TargetOutputPath>
</_BlazorPackageContentOutput>
<BlazorItemOutput Include="@(_BlazorPackageContentOutput)" />
<BlazorOutputWithTargetPath Include="@(_BlazorPackageContentOutput)" />
</ItemGroup>
</Target>
<PropertyGroup Label="Intermediate output paths">
<Target Name="_ResolveBlazorInputs" DependsOnTargets="ResolveReferences;ResolveRuntimePackAssets">
<PropertyGroup>
<!-- /obj/<<configuration>>/<<targetframework>>/blazor -->
<BlazorIntermediateOutputPath>$(IntermediateOutputPath)$(BaseBlazorIntermediateOutputPath)</BlazorIntermediateOutputPath>
<BlazorIntermediateOutputPath Condition="! $([System.IO.Path]::IsPathRooted($(BlazorIntermediateOutputPath)))">$([MSBuild]::Escape($([System.IO.Path]::GetFullPath('$([System.IO.Path]::Combine('$(MSBuildProjectDirectory)', '$(BlazorIntermediateOutputPath)'))'))))</BlazorIntermediateOutputPath>
<!-- Common marker files paths -->
<!-- /obj/<<configuration>>/<<targetframework>>/blazor/inputs.basic.cache -->
<BlazorBuildCommonInputsCache>$(BlazorIntermediateOutputPath)inputs.basic.cache</BlazorBuildCommonInputsCache>
<!-- /obj/<<configuration>>/<<targetframework>>/blazor/inputs.copylocal.txt -->
<BlazorLocalReferencesOutputPath>$(BlazorIntermediateOutputPath)inputs.copylocal.txt</BlazorLocalReferencesOutputPath>
<!-- /obj/<<configuration>>/<<targetframework>>/blazor/inputs.linkerswitch.cache -->
<BlazorBuildLinkerSwitchInputsCache>$(BlazorIntermediateOutputPath)inputs.linkerswitch.cache</BlazorBuildLinkerSwitchInputsCache>
<!-- Linker paths and marker files -->
<!-- /obj/<<configuration>>/<<targetframework>>/blazor/inputs.linker.cache -->
<BlazorBuildLinkerInputsCache>$(BlazorIntermediateOutputPath)inputs.linker.cache</BlazorBuildLinkerInputsCache>
<BlazorIntermediateOutputPath>$(IntermediateOutputPath)blazor\</BlazorIntermediateOutputPath>
<!-- /obj/<<configuration>>/<<targetframework>>/blazor/linker.descriptor.xml -->
<GeneratedBlazorLinkerDescriptor>$(BlazorIntermediateOutputPath)linker.descriptor.xml</GeneratedBlazorLinkerDescriptor>
<_TypeGranularityLinkerDescriptor>$(BlazorIntermediateOutputPath)linker.typegranularityconfig.xml</_TypeGranularityLinkerDescriptor>
<!-- /obj/<<configuration>>/<<targetframework>>/blazor/linker/ -->
<BlazorIntermediateLinkerOutputPath>$(BlazorIntermediateOutputPath)linker/</BlazorIntermediateLinkerOutputPath>
<!-- /obj/<<configuration>>/<<targetframework>>/blazor/linked.assemblies.txt -->
<BlazorIntermediateLinkerResultFilePath>$(BlazorIntermediateOutputPath)linked.assemblies.txt</BlazorIntermediateLinkerResultFilePath>
<!-- Resolved assemblies paths and marker files -->
<!-- /obj/<<configuration>>/<<targetframework>>/blazor/resolvedassemblies/ -->
<BlazorIntermediateResolvedApplicationAssembliesOutputPath>$(BlazorIntermediateOutputPath)resolvedassemblies/</BlazorIntermediateResolvedApplicationAssembliesOutputPath>
<!-- /obj/<<configuration>>/<<targetframework>>/blazor/resolved.assemblies.txt -->
<BlazorResolvedAssembliesOutputPath>$(BlazorIntermediateOutputPath)resolved.assemblies.txt</BlazorResolvedAssembliesOutputPath>
<!-- boot json related paths and markers -->
<!-- /obj/<<configuration>>/<<targetframework>>/blazor/ -->
<BlazorBootJsonIntermediateOutputDir>$(BlazorIntermediateOutputPath)</BlazorBootJsonIntermediateOutputDir>
<!-- /obj/<<configuration>>/<<targetframework>>/blazor/blazor.boot.json -->
<BlazorBootJsonIntermediateOutputPath>$(BlazorBootJsonIntermediateOutputDir)$(BlazorBootJsonName)</BlazorBootJsonIntermediateOutputPath>
<BlazorBootJsonIntermediateOutputPath>$(BlazorIntermediateOutputPath)$(BlazorBootJsonName)</BlazorBootJsonIntermediateOutputPath>
<!-- /obj/<<configuration>>/<<targetframework>>/blazor/inputs.bootjson.cache -->
<BlazorBuildBootJsonInputsCache>$(BlazorIntermediateOutputPath)inputs.bootjson.cache</BlazorBuildBootJsonInputsCache>
<!-- /obj/<<configuration>>/<<targetframework>>/blazor/resolve-dependencies.txt -->
<BlazorResolveDependenciesFilePath>$(BlazorIntermediateOutputPath)resolve-dependencies.txt</BlazorResolveDependenciesFilePath>
<!-- /obj/<<configuration>>/<<targetframework>>/blazor/bootjson-references.txt -->
<BlazorBootJsonReferencesFilePath>$(BlazorIntermediateOutputPath)bootjson-references.txt</BlazorBootJsonReferencesFilePath>
<!-- /obj/<<configuration>>/<<targetframework>>/blazor/embedded.resources.txt -->
<BlazorEmbeddedResourcesConfigFilePath>$(BlazorIntermediateOutputPath)embedded.resources.txt</BlazorEmbeddedResourcesConfigFilePath>
<_BlazorLinkerOutputCache>$(BlazorIntermediateOutputPath)linker.output</_BlazorLinkerOutputCache>
<_BlazorApplicationAssembliesCacheFile>$(BlazorIntermediateOutputPath)unlinked.output</_BlazorApplicationAssembliesCacheFile>
</PropertyGroup>
<PropertyGroup Label="Final output paths">
<BlazorRuntimeBinOutputPath>$(TargetDir)$(BaseBlazorRuntimeBinOutputPath)</BlazorRuntimeBinOutputPath>
</PropertyGroup>
<ItemGroup>
<_WebAssemblyBCLFolder Include="
$(DotNetWebAssemblyBCLPath);
$(DotNetWebAssemblyBCLFacadesPath);
$(DotNetWebAssemblyFrameworkPath)" />
<_WebAssemblyBCLAssembly Include="%(_WebAssemblyBCLFolder.Identity)*.dll" />
</ItemGroup>
<!--
Calculate the assemblies that act as inputs to calculate assembly closure. Based on _ComputeAssembliesToPostprocessOnPublish which is used as input to SDK's linker
https://github.com/dotnet/sdk/blob/d597e7b09d7657ba4e326d6734e14fcbf8473564/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Publish.targets#L864-L873
-->
<ItemGroup>
<!-- Assemblies from packages -->
<_BlazorManagedRuntimeAssemby Include="@(RuntimeCopyLocalItems)" />
<!-- Assemblies from other references -->
<_BlazorUserRuntimeAssembly Include="@(ReferencePath->WithMetadataValue('CopyLocal', 'true'))" />
<_BlazorUserRuntimeAssembly Include="@(ReferenceDependencyPaths->WithMetadataValue('CopyLocal', 'true'))" />
<_BlazorManagedRuntimeAssemby Include="@(_BlazorUserRuntimeAssembly)" />
<_BlazorManagedRuntimeAssemby Include="@(IntermediateAssembly)" />
</ItemGroup>
<MakeDir Directories="$(BlazorIntermediateOutputPath)" />
</Target>
<Target Name="_DefineBlazorCommonInputs">
<!-- If ResolveReferences hasn't yet run, we must be inside a VS publish process
that doesn't also do a build, so use the stored information. -->
<ReadLinesFromFile
Condition="'$(_BlazorResolveReferencesDidRun)'!='true'"
File="$(BlazorLocalReferencesOutputPath)">
<Output TaskParameter="Lines" ItemName="_BlazorDependencyInput"/>
</ReadLinesFromFile>
<ItemGroup Condition="'$(_BlazorResolveReferencesDidRun)'=='true'">
<!-- ... otherwise we can get the fresh info from @(ReferenceCopyLocalPaths) -->
<_BlazorDependencyInput Include="@(ReferenceCopyLocalPaths->WithMetadataValue('Extension','.dll')->'%(FullPath)')" />
</ItemGroup>
<Target Name="_ResolveBlazorOutputs" DependsOnTargets="_ResolveBlazorOutputsWhenLinked;_ResolveBlazorOutputsWhenNotLinked">
<Error
Message="Unrecongnized value for BlazorLinkOnBuild: '$(BlazorLinkOnBuild)'. Valid values are 'true' or 'false'."
Condition="'$(BlazorLinkOnBuild)' != 'true' AND '$(BlazorLinkOnBuild)' != 'false'" />
<ItemGroup>
<_BlazorCommonInput Include="@(IntermediateAssembly)" />
<_BlazorCommonInput Include="@(_BlazorDependencyInput)" />
<_BlazorCommonInput Include="$(_BlazorShouldLinkApplicationAssemblies)" />
<_BlazorCommonInput Include="$(BlazorEnableDebugging)" />
<_BlazorLinkingOption Condition="'$(_BlazorShouldLinkApplicationAssemblies)' == ''" Include="false" />
<_BlazorLinkingOption Condition="'$(_BlazorShouldLinkApplicationAssemblies)' != ''" Include="true" />
<!--
ReferenceCopyLocalPaths includes all files that are part of the build out with CopyLocalLockFileAssemblies on.
Remove assemblies that are inputs to calculating the assembly closure. Instead use the resolved outputs, since it is the minimal set.
-->
<_BlazorCopyLocalPaths Include="@(ReferenceCopyLocalPaths)" />
<_BlazorCopyLocalPaths Remove="@(_BlazorManagedRuntimeAssemby)" />
<BlazorOutputWithTargetPath Include="@(_BlazorCopyLocalPaths)">
<BlazorRuntimeFile>true</BlazorRuntimeFile>
<TargetOutputPath>$(BlazorRuntimeBinOutputPath)%(_BlazorCopyLocalPaths.DestinationSubDirectory)%(FileName)%(Extension)</TargetOutputPath>
<RelativeOutputPath>%(_BlazorCopyLocalPaths.DestinationSubDirectory)%(FileName)%(Extension)</RelativeOutputPath>
</BlazorOutputWithTargetPath>
<BlazorOutputWithTargetPath Include="@(_BlazorResolvedAssembly)">
<BlazorRuntimeFile>true</BlazorRuntimeFile>
<TargetOutputPath>$(BlazorRuntimeBinOutputPath)%(FileName)%(Extension)</TargetOutputPath>
<RelativeOutputPath>%(FileName)%(Extension)</RelativeOutputPath>
</BlazorOutputWithTargetPath>
</ItemGroup>
<Hash ItemsToHash="@(_BlazorCommonInput)">
<Output TaskParameter="HashResult" PropertyName="_BlazorBuildBasicInputHash" />
</Hash>
<WriteLinesToFile
Lines="$(_BlazorBuildBasicInputHash)"
File="$(BlazorBuildCommonInputsCache)"
Overwrite="True"
WriteOnlyWhenDifferent="True" />
<WriteLinesToFile
Lines="@(_BlazorDependencyInput)"
File="$(BlazorLocalReferencesOutputPath)"
Overwrite="True"
WriteOnlyWhenDifferent="True" />
<!-- Switch to detect when we switch from linking to not linking and viceversa -->
<WriteLinesToFile
Lines="@(_BlazorLinkingOption)"
File="$(BlazorBuildLinkerSwitchInputsCache)"
Overwrite="True"
WriteOnlyWhenDifferent="True" />
<ItemGroup>
<FileWrites Include="$(BlazorBuildLinkerSwitchInputsCache)" />
<FileWrites Include="$(BlazorBuildCommonInputsCache)" />
<FileWrites Include="$(BlazorLocalReferencesOutputPath)" />
</ItemGroup>
</Target>
<Target Name="_BlazorResolveOutputBinaries" DependsOnTargets="_CollectLinkerOutputs;_CollectResolvedAssemblies" />
<!--
Linker enabled part of the pipeline:
* If there are no descriptors defined, generate a new linker descriptor.
* Collect the list of descriptors and produce a marker file to determine when the
inputs to the linker change in future builds.
* Invoke the linker if the linker inputs marker file is newer than the linker outputs.
* Read the outputs from the linker and add them to the list of blazor outputs.
* Invoke the linker and write linked files to a well-known directory.
* Collect the outputs of the linker.
-->
<PropertyGroup>
<_CollectLinkerOutputsDependsOn>
_GenerateLinkerDescriptor;
_CollectBlazorLinkerDescriptors;
_LinkBlazorApplication
</_CollectLinkerOutputsDependsOn>
</PropertyGroup>
<Target
Name="_CollectLinkerOutputs"
Condition="'$(_BlazorShouldLinkApplicationAssemblies)' != ''"
DependsOnTargets="$(_CollectLinkerOutputsDependsOn)">
<!--
Read the outputs from the linker (from this run or a previous run) and set them in an item group for
later use.
-->
<ReadLinesFromFile File="$(BlazorIntermediateLinkerResultFilePath)">
<Output TaskParameter="Lines" ItemName="_OptimizedFiles"/>
Name="_ResolveBlazorOutputsWhenLinked"
Condition="'$(BlazorLinkOnBuild)' == 'true'"
DependsOnTargets="_PrepareBlazorLinkerInputs;_GenerateBlazorLinkerDescriptor;_GenerateTypeGranularLinkerDescriptor;_LinkBlazorApplication">
<!-- _BlazorLinkerOutputCache records files linked during the last incremental build of the target. Read the contents and assign linked files to be copied to the output. -->
<ReadLinesFromFile File="$(_BlazorLinkerOutputCache)">
<Output TaskParameter="Lines" ItemName="_BlazorResolvedAssembly"/>
</ReadLinesFromFile>
</Target>
<Target Name="_PrepareBlazorLinkerInputs">
<ItemGroup>
<BlazorItemOutput Include="@(_OptimizedFiles->WithMetadataValue('Extension','.dll'))">
<TargetOutputPath>$(BlazorRuntimeBinOutputPath)%(FileName)%(Extension)</TargetOutputPath>
<Type>Assembly</Type>
<PrimaryOutput Condition="'%(FileName)' == @(IntermediateAssembly->'%(FileName)')">true</PrimaryOutput>
</BlazorItemOutput>
<BlazorItemOutput Include="@(_OptimizedFiles->WithMetadataValue('Extension','.pdb'))">
<TargetOutputPath>$(BlazorRuntimeBinOutputPath)%(FileName)%(Extension)</TargetOutputPath>
<Type>Pdb</Type>
</BlazorItemOutput>
<FileWrites Include="@(BlazorItemOutput->WithMetadataValue('Type','Assembly')->'%(TargetOutputPath)')" />
<_BlazorRuntimeCopyLocalItems Include="@(RuntimeCopyLocalItems)" />
<!--
Any assembly from a package reference that starts with System. file name is allowed to be linked.
Assemblies from Microsoft.AspNetCore and Microsoft.Extensions, are also linked but with TypeGranularity.
-->
<_BlazorRuntimeCopyLocalItems IsLinkable="true" Condition="$([System.String]::Copy('%(Filename)').StartsWith('System.'))" />
<_BlazorRuntimeCopyLocalItems IsLinkable="true" TypeGranularity="true" Condition="$([System.String]::Copy('%(Filename)').StartsWith('Microsoft.AspNetCore.'))" />
<_BlazorRuntimeCopyLocalItems IsLinkable="true" TypeGranularity="true" Condition="$([System.String]::Copy('%(Filename)').StartsWith('Microsoft.Extensions.'))" />
<_BlazorAssemblyToLink Include="@(_WebAssemblyBCLAssembly)" />
<_BlazorAssemblyToLink Include="@(_BlazorRuntimeCopyLocalItems)" Condition="'%(_BlazorRuntimeCopyLocalItems.IsLinkable)' == 'true'" />
<_BlazorLinkerRoot Include="@(IntermediateAssembly)" />
<_BlazorLinkerRoot Include="@(_BlazorUserRuntimeAssembly)" />
<_BlazorLinkerRoot Include="@(_BlazorRuntimeCopyLocalItems)" Condition="'%(_BlazorRuntimeCopyLocalItems.IsLinkable)' != 'true'" />
</ItemGroup>
</Target>
<Target Name="_GenerateLinkerDescriptor"
Inputs="$(BlazorBuildCommonInputsCache)"
<UsingTask TaskName="BlazorCreateRootDescriptorFile" AssemblyFile="$(BlazorTasksPath)" />
<Target Name="_GenerateBlazorLinkerDescriptor"
Inputs="@(IntermediateAssembly)"
Outputs="$(GeneratedBlazorLinkerDescriptor)"
Condition="$(_BlazorShouldLinkApplicationAssemblies) != '' and '@(BlazorLinkerDescriptor)' == ''">
Condition="'@(BlazorLinkerDescriptor)' == ''">
<!-- Generate linker descriptors if the project doesn't explicitly provide one. -->
<BlazorCreateRootDescriptorFile
AssemblyNames="@(IntermediateAssembly->'%(Filename)')"
RootDescriptorFilePath="$(GeneratedBlazorLinkerDescriptor)" />
<ItemGroup>
<_PrepareLinkerDescriptorAssemblyLine Include="@(IntermediateAssembly->'%(FileName)')" />
<_GeneratedLinkerDescriptorLine Include="&lt;linker&gt;" />
<_GeneratedLinkerDescriptorLine Include="@(_PrepareLinkerDescriptorAssemblyLine->'&lt;assembly fullname=&quot;%(Identity)&quot; /&gt;')" />
<_GeneratedLinkerDescriptorLine Include="&lt;/linker&gt;" />
</ItemGroup>
<WriteLinesToFile
Lines="@(_GeneratedLinkerDescriptorLine)"
File="$(GeneratedBlazorLinkerDescriptor)"
Overwrite="true"
WriteOnlyWhenDifferent="True" />
</Target>
<Target Name="_CollectBlazorLinkerDescriptors">
<ItemGroup Condition="@(BlazorLinkerDescriptor) == ''">
<BlazorLinkerDescriptor Include="$(_BlazorBuiltInBclLinkerDescriptor)" />
<BlazorLinkerDescriptor Include="$(GeneratedBlazorLinkerDescriptor)" />
<FileWrites Include="$(GeneratedBlazorLinkerDescriptor)" />
<BlazorLinkerDescriptor Include="$(GeneratedBlazorLinkerDescriptor)" />
<BlazorLinkerDescriptor Include="$(_BlazorBuiltInBclLinkerDescriptor)" />
</ItemGroup>
<ItemGroup>
<_BlazorLinkerInput Include="@(IntermediateAssembly)" />
<_BlazorLinkerInput Include="@(_BlazorDependencyInput)" />
<_BlazorLinkerInput Include="@(BlazorLinkerDescriptor)" />
<_BlazorLinkerInput Include="$(AdditionalMonoLinkerOptions)" />
</ItemGroup>
<Hash ItemsToHash="@(_BlazorLinkerInput)">
<Output TaskParameter="HashResult" PropertyName="_BlazorLinkerInputHash" />
</Hash>
<WriteLinesToFile
Lines="$(_BlazorLinkerInputHash)"
File="$(BlazorBuildLinkerInputsCache)"
Overwrite="True"
WriteOnlyWhenDifferent="True" />
<ItemGroup>
<FileWrites Include="$(BlazorBuildLinkerInputsCache)" />
</ItemGroup>
</Target>
<UsingTask TaskName="GenerateTypeGranularityLinkingConfig" AssemblyFile="$(BlazorTasksPath)" />
<Target Name="_GenerateTypeGranularLinkerDescriptor"
Inputs="@(_BlazorAssemblyToLink->WithMetadataValue('TypeGranularity', 'true'))"
Outputs="$(_TypeGranularityLinkerDescriptor)">
<GenerateTypeGranularityLinkingConfig
Assemblies="@(_BlazorAssemblyToLink->WithMetadataValue('TypeGranularity', 'true'))"
OutputPath="$(_TypeGranularityLinkerDescriptor)" />
<ItemGroup>
<BlazorLinkerDescriptor Include="$(_TypeGranularityLinkerDescriptor)" />
<FileWrites Include="$(_TypeGranularityLinkerDescriptor)" />
</ItemGroup>
</Target>
<UsingTask TaskName="BlazorILLink" AssemblyFile="$(BlazorTasksPath)" />
<Target
Name="_LinkBlazorApplication"
Condition="$(_BlazorShouldLinkApplicationAssemblies) != ''"
Inputs="$(BlazorBuildLinkerInputsCache);
@(IntermediateAssembly);
@(_BlazorDependencyInput);
@(BlazorLinkerDescriptor)"
Outputs="$(BlazorIntermediateLinkerResultFilePath)"
>
<!--
At this point we have decided to run the mono linker on the Blazor assembly and its dependencies.
The steps to run the mono linker are the following:
1) Clear the linker output directory if not clean before hand, as we don't know what the outputs of
the linker will be.
2) Run the linker on the main assembly, its dependencies and pass in the BCL folders to do the lookup
for framework assemblies.
3) Once we've run the linker we need to capture the produced output and generate a marker file containing
the list of produced files. This file will act as a marker to skip running the linker if none of the inputs
has changed.
4) Add the file we just created to the list of file writes, to support incremental builds.
-->
<ItemGroup>
<_MonoBaseClassLibraryFolder Include="$(MonoBaseClassLibraryPath);$(MonoBaseClassLibraryFacadesPath);$(MonoWasmFrameworkPath)" />
<_BlazorAssembliesToLink Include="@(_BlazorDependencyInput->'-a &quot;%(Identity)&quot;')" />
<_BlazorAssembliesToLink Include="@(IntermediateAssembly->'-a &quot;%(FullPath)&quot;')" />
<_BlazorFolderLookupPaths Include="@(_MonoBaseClassLibraryFolder->'-d &quot;%(Identity)&quot;')" />
<_BlazorAssemblyDescriptorFiles
Include="@(BlazorLinkerDescriptor->'-x &quot;%(FullPath)&quot;')" Condition="'@(BlazorLinkerDescriptor)' != ''" />
</ItemGroup>
Inputs="$(ProjectAssetsFile);
@(_BlazorManagedRuntimeAssemby);
@(BlazorLinkerDescriptor);
$(MSBuildAllProjects)"
Outputs="$(_BlazorLinkerOutputCache)">
<PropertyGroup>
<_BlazorLinkerAdditionalOptions>-l $(MonoLinkerI18NAssemblies) $(AdditionalMonoLinkerOptions)</_BlazorLinkerAdditionalOptions>
</PropertyGroup>
<!-- Clear the contents of /obj/<<configuration>>/<<targetframework>>/blazor/blazor/linker -->
<Delete Files="$(BlazorIntermediateLinkerOutputPath)*.dll" />
<!-- Run the linker and put the results in /obj/<<configuration>>/<<targetframework>>/blazor/blazor/linker -->
<Exec Command="dotnet &quot;$(MonoLinkerPath)&quot; $(_BlazorLinkerAdditionalOptions) @(_BlazorFolderLookupPaths, ' ') -o &quot;$(BlazorIntermediateLinkerOutputPath)&quot; @(_BlazorAssemblyDescriptorFiles, ' ') @(_BlazorAssembliesToLink, ' ')" />
<!-- Collect the contents of /obj/<<configuration>>/<<targetframework>>/blazor/blazor/linker/ -->
<ItemGroup>
<_BlazorLinkerOutput Include="$(BlazorIntermediateLinkerOutputPath)*.dll" />
<_BlazorLinkerOutput Include="$(BlazorIntermediateLinkerOutputPath)*.pdb" />
<_OldLinkedFile Include="$(BlazorIntermediateLinkerOutputPath)*.dll" />
<_OldLinkedFile Include="$(BlazorIntermediateLinkerOutputPath)*.pdb" />
</ItemGroup>
<Delete Files="@(_OldLinkedFile)" />
<!--
Write the list of files in /obj/<<configuration>>/<<targetframework>>/blazor/blazor/linker/ into
/obj/<<configuration>>/<<targetframework>>/blazor/blazor/linked.assemblies.txt
When running from Desktop MSBuild, DOTNET_HOST_PATH is not set.
In this case, explicitly specify the path to the dotnet host.
-->
<WriteLinesToFile
File="$(BlazorIntermediateLinkerResultFilePath)"
Lines="@(_BlazorLinkerOutput)"
Overwrite="true" />
<PropertyGroup Condition=" '$(DOTNET_HOST_PATH)' == '' ">
<_DotNetHostDirectory>$(NetCoreRoot)</_DotNetHostDirectory>
<_DotNetHostFileName>dotnet</_DotNetHostFileName>
<_DotNetHostFileName Condition=" '$(OS)' == 'Windows_NT' ">dotnet.exe</_DotNetHostFileName>
</PropertyGroup>
<BlazorILLink
ILLinkPath="$(MonoLinkerPath)"
AssemblyPaths="@(_BlazorAssemblyToLink)"
RootAssemblyNames="@(_BlazorLinkerRoot)"
RootDescriptorFiles="@(BlazorLinkerDescriptor)"
OutputDirectory="$(BlazorIntermediateLinkerOutputPath)"
ExtraArgs="$(_BlazorLinkerAdditionalOptions)"
ToolExe="$(_DotNetHostFileName)"
ToolPath="$(_DotNetHostDirectory)" />
<!-- Add /obj/<<configuration>>/<<targetframework>>/blazor/blazor/linked.assemblies.txt to the list of written files. -->
<!-- Add /obj/<<configuration>>/<<targetframework>>/blazor/blazor/linker/*.dll to the list of written files. -->
<ItemGroup>
<FileWrites Include="$(BlazorIntermediateLinkerResultFilePath)" />
<FileWrites Include="@(_BlazorLinkerOutput)" />
<_LinkerResult Include="$(BlazorIntermediateLinkerOutputPath)*.dll" />
<_LinkerResult Include="$(BlazorIntermediateLinkerOutputPath)*.pdb" Condition="'$(BlazorEnableDebugging)' == 'true'" />
</ItemGroup>
<WriteLinesToFile File="$(_BlazorLinkerOutputCache)" Lines="@(_LinkerResult)" Overwrite="true" />
</Target>
<!--
Linker disabled part of the pipeline:
<UsingTask TaskName="ResolveBlazorRuntimeDependencies" AssemblyFile="$(BlazorTasksPath)" />
<Target
Name="_ResolveBlazorOutputsWhenNotLinked"
DependsOnTargets="_ResolveBlazorRuntimeDependencies"
Condition="'$(BlazorLinkOnBuild)' != 'true'">
* Run a CLI tool to produce the transitive closure of application references using the main application
as entry point.
* Read the list of resolved application references from the file produced by the previous step.
* Copy the resolved application references into an intermediate folder.
* If we are switching from linking to not linking
Touch the files in the intermediate folder to ensure they are copied to the output and replace
the linked versions with the same name.
* Collect the list of resolved assemblies in the intermediate output folder and prepare them to be
copied to their final destination in the output folder.
-->
<PropertyGroup>
<_CollectResolvedAssembliesDependsOn>
_ResolveBlazorApplicationAssemblies;
_ReadResolvedBlazorApplicationAssemblies;
_IntermediateCopyBlazorApplicationAssemblies;
_TouchBlazorApplicationAssemblies
</_CollectResolvedAssembliesDependsOn>
</PropertyGroup>
<ReadLinesFromFile File="$(_BlazorApplicationAssembliesCacheFile)" Condition="'@(_BlazorResolvedAssembly->Count())' == '0'">
<Output TaskParameter="Lines" ItemName="_BlazorResolvedAssembly"/>
</ReadLinesFromFile>
</Target>
<Target
Name="_CollectResolvedAssemblies"
DependsOnTargets="$(_CollectResolvedAssembliesDependsOn)"
Condition="'$(_BlazorShouldLinkApplicationAssemblies)' == ''">
Name="_ResolveBlazorRuntimeDependencies"
Inputs="$(ProjectAssetsFile);
@(IntermediateAssembly);
@(_BlazorManagedRuntimeAssemby)"
Outputs="$(_BlazorApplicationAssembliesCacheFile)">
<!--
At this point we have decided not to run the linker and instead to just copy the assemblies
from the BCL referenced by the app the nuget package into the _framework/_bin folder.
The only thing we need to do here is collect the list of items that will go into _framework/_bin.
-->
<ResolveBlazorRuntimeDependencies
EntryPoint="@(IntermediateAssembly)"
ApplicationDependencies="@(_BlazorManagedRuntimeAssemby)"
WebAssemblyBCLAssemblies="@(_WebAssemblyBCLAssembly)">
<Output TaskParameter="Dependencies" ItemName="_BlazorResolvedAssembly" />
</ResolveBlazorRuntimeDependencies>
<WriteLinesToFile File="$(_BlazorApplicationAssembliesCacheFile)" Lines="@(_BlazorResolvedRuntimeDependencies)" Overwrite="true" />
<ItemGroup>
<BlazorItemOutput Include="@(_IntermediateResolvedRuntimeDependencies->WithMetadataValue('Extension','.dll'))">
<TargetOutputPath>$(BlazorRuntimeBinOutputPath)%(FileName)%(Extension)</TargetOutputPath>
<Type>Assembly</Type>
<PrimaryOutput Condition="'%(FileName)' == @(IntermediateAssembly->'%(FileName)')">true</PrimaryOutput>
</BlazorItemOutput>
<BlazorItemOutput Include="@(_IntermediateResolvedRuntimeDependencies->WithMetadataValue('Extension','.pdb'))">
<TargetOutputPath>$(BlazorRuntimeBinOutputPath)%(FileName)%(Extension)</TargetOutputPath>
<Type>Pdb</Type>
</BlazorItemOutput>
<FileWrites Include="@(BlazorItemOutput->WithMetadataValue('Type','Assembly')->'%(TargetOutputPath)')" />
<FileWrites Include="$(_BlazorApplicationAssembliesCacheFile)" />
</ItemGroup>
</Target>
<Target
Name="_ResolveBlazorApplicationAssemblies"
Condition="'$(_BlazorShouldLinkApplicationAssemblies)' == ''"
Inputs="$(BlazorBuildCommonInputsCache);
@(IntermediateAssembly);
@(_BlazorDependencyInput)"
Outputs="$(BlazorResolvedAssembliesOutputPath)"
>
<PropertyGroup>
<_ReferencesArg Condition="'@(_BlazorDependencyInput)' != ''">--references &quot;$(BlazorResolveDependenciesFilePath)&quot;</_ReferencesArg>
<_BclParameter>--base-class-library &quot;$(MonoBaseClassLibraryPath)&quot; --base-class-library &quot;$(MonoBaseClassLibraryFacadesPath)&quot; --base-class-library &quot;$(MonoWasmFrameworkPath)&quot;</_BclParameter>
</PropertyGroup>
<WriteLinesToFile
File="$(BlazorResolveDependenciesFilePath)"
Lines="@(_BlazorDependencyInput)"
Overwrite="true" />
<Exec Command="$(BlazorBuildExe) resolve-dependencies &quot;@(IntermediateAssembly->'%(FullPath)')&quot; $(_ReferencesArg) $(_BclParameter) --output &quot;$(BlazorResolvedAssembliesOutputPath)&quot;" />
</Target>
<Target Name="_ReadResolvedBlazorApplicationAssemblies">
<ReadLinesFromFile File="$(BlazorResolvedAssembliesOutputPath)">
<Output TaskParameter="Lines" ItemName="_BlazorResolvedRuntimeDependencies"/>
</ReadLinesFromFile>
<ItemGroup>
<_IntermediateResolvedRuntimeDependencies Include="@(_BlazorResolvedRuntimeDependencies->'$(BlazorIntermediateResolvedApplicationAssembliesOutputPath)%(FileName)%(Extension)')" />
</ItemGroup>
<ItemGroup>
<FileWrites Include="$(BlazorResolvedAssembliesOutputPath)" />
<FileWrites Include="@(_IntermediateResolvedRuntimeDependencies)" />
</ItemGroup>
</Target>
<Target
Name="_IntermediateCopyBlazorApplicationAssemblies"
Inputs="@(_BlazorResolvedRuntimeDependencies)"
Outputs="@(_BlazorResolvedRuntimeDependencies->'$(BlazorIntermediateResolvedApplicationAssembliesOutputPath)%(FileName)%(Extension)')">
<Copy
SourceFiles="@(_BlazorResolvedRuntimeDependencies)"
DestinationFiles="@(_BlazorResolvedRuntimeDependencies->'$(BlazorIntermediateResolvedApplicationAssembliesOutputPath)%(FileName)%(Extension)')"
SkipUnchangedFiles="$(SkipCopyUnchangedFiles)"
OverwriteReadOnlyFiles="$(OverwriteReadOnlyFiles)"
Retries="$(CopyRetryCount)"
RetryDelayMilliseconds="$(CopyRetryDelayMilliseconds)"
UseHardlinksIfPossible="$(CreateHardLinksForCopyFilesToOutputDirectoryIfPossible)"
UseSymboliclinksIfPossible="$(CreateSymbolicLinksForCopyFilesToOutputDirectoryIfPossible)" />
</Target>
<Target
Name="_TouchBlazorApplicationAssemblies"
Inputs="$(BlazorBuildLinkerSwitchInputsCache)"
Outputs="@(_IntermediateResolvedRuntimeDependencies)">
<Touch Files="@(_IntermediateResolvedRuntimeDependencies)" ForceTouch="true" />
</Target>
<!--
Final part of the build pipeline:
* Collect the blazor application assemblies to be copied to the output and create a marker file.
* Call our CLI tool to generate the boot json if the list of assemblies has changed.
-->
<Target Name="_ResolveBlazorBootJsonInputs">
<ItemGroup>
<BlazorBootJsonInput Include="$(Configuration)" />
<BlazorBootJsonInput Include="@(BlazorItemOutput->WithMetadataValue('Type','Assembly')->'%(FullPath)')" />
<BlazorBootJsonInput Include="@(BlazorItemOutput->WithMetadataValue('Type','Pdb')->'%(FullPath)')" />
<BlazorBootJsonInput Include="@(_BlazorLinkingOption)" />
<BlazorBootJsonInput Include="$(BlazorEnableDebugging)" />
</ItemGroup>
<WriteLinesToFile
File="$(BlazorBuildBootJsonInputsCache)"
Lines="@(BlazorBootJsonInput)"
Overwrite="true"
WriteOnlyWhenDifferent="True" />
<ItemGroup>
<FileWrites Include="$(BlazorBuildBootJsonInputsCache)" />
</ItemGroup>
</Target>
<UsingTask TaskName="GenerateBlazorBootJson" AssemblyFile="$(BlazorTasksPath)" />
<Target
Name="_GenerateBlazorBootJson"
DependsOnTargets="_ResolveBlazorBootJsonInputs"
Inputs="$(BlazorBuildBootJsonInputsCache);@(_BlazorDependencyInput)"
Inputs="@(BlazorOutputWithTargetPath)"
Outputs="$(BlazorBootJsonIntermediateOutputPath)">
<ItemGroup>
<_UnlinkedAppReferencesPaths Include="@(_BlazorDependencyInput)" />
<_AppReferences Include="@(BlazorItemOutput->WithMetadataValue('Type','Assembly')->WithMetadataValue('PrimaryOutput','')->'%(FileName)%(Extension)')" />
<_AppReferences Include="@(BlazorItemOutput->WithMetadataValue('Type','Pdb')->'%(FileName)%(Extension)')" Condition="'$(BlazorEnableDebugging)' == 'true'" />
<_BlazorRuntimeFile Include="@(BlazorOutputWithTargetPath->WithMetadataValue('BlazorRuntimeFile', 'true'))" />
</ItemGroup>
<PropertyGroup>
<_LinkerEnabledFlag Condition="'$(_BlazorShouldLinkApplicationAssemblies)' != ''">--linker-enabled</_LinkerEnabledFlag>
<_ReferencesArg Condition="'@(_AppReferences)' != ''">--references &quot;$(BlazorBootJsonReferencesFilePath)&quot;</_ReferencesArg>
<_EmbeddedResourcesArg Condition="'@(_UnlinkedAppReferencesPaths)' != ''">--embedded-resources &quot;$(BlazorEmbeddedResourcesConfigFilePath)&quot;</_EmbeddedResourcesArg>
</PropertyGroup>
<WriteLinesToFile
File="$(BlazorBootJsonReferencesFilePath)"
Lines="@(_AppReferences)"
Overwrite="true" />
<GenerateBlazorBootJson
AssemblyPath="@(IntermediateAssembly)"
References="@(_BlazorRuntimeFile)"
LinkerEnabled="$(BlazorLinkOnBuild)"
OutputPath="$(BlazorBootJsonIntermediateOutputPath)" />
<WriteLinesToFile
Condition="'@(_UnlinkedAppReferencesPaths)' != ''"
File="$(BlazorEmbeddedResourcesConfigFilePath)"
Lines="@(_UnlinkedAppReferencesPaths)"
Overwrite="true" />
<Exec Command="$(BlazorBuildExe) write-boot-json &quot;@(IntermediateAssembly)&quot; $(_ReferencesArg) $(_EmbeddedResourcesArg) $(_LinkerEnabledFlag) --output &quot;$(BlazorBootJsonIntermediateOutputPath)&quot;" />
<ItemGroup Condition="Exists('$(BlazorBootJsonIntermediateOutputPath)')">
<_BlazorBootJson Include="$(BlazorBootJsonIntermediateOutputPath)" />
<_BlazorBootJsonEmbeddedContentFile Include="$(BlazorBootJsonIntermediateOutputDir)_content\**\*.*" />
<BlazorItemOutput Include="@(_BlazorBootJson)">
<TargetOutputPath>$(TargetDir)$(BlazorBootJsonOutputPath)</TargetOutputPath>
<Type>BootJson</Type>
</BlazorItemOutput>
<BlazorItemOutput Include="@(_BlazorBootJsonEmbeddedContentFile)">
<TargetOutputPath>$(TargetDir)dist/_content/%(RecursiveDir)%(FileName)%(Extension)</TargetOutputPath>
</BlazorItemOutput>
<ItemGroup>
<BlazorOutputWithTargetPath Include="$(BlazorBootJsonIntermediateOutputPath)" TargetOutputPath="$(BaseBlazorRuntimeOutputPath)$(BlazorBootJsonName)" />
<FileWrites Include="$(BlazorBootJsonIntermediateOutputPath)" />
<FileWrites Include="@(_BlazorBootJsonEmbeddedContentFile)" />
</ItemGroup>
</Target>
</Project>

View File

@ -26,9 +26,8 @@
</ContentWithTargetPath>
<!-- Publish all the 'dist' files -->
<_BlazorGCTPDIDistFiles Include="@(BlazorItemOutput->'%(TargetOutputPath)')" />
<_BlazorGCTPDI Include="@(_BlazorGCTPDIDistFiles)">
<TargetPath>$(BlazorPublishDistDir)$([MSBuild]::MakeRelative('$(TargetDir)dist\', %(Identity)))</TargetPath>
<_BlazorGCTPDI Include="%(BlazorOutputWithTargetPath.Identity)">
<TargetPath>$(AssemblyName)\%(TargetOutputPath)</TargetPath>
</_BlazorGCTPDI>
<ContentWithTargetPath Include="@(_BlazorGCTPDI)">
@ -41,8 +40,17 @@
<PropertyGroup>
<_BlazorConfigPath>$(OutDir)$(AssemblyName).blazor.config</_BlazorConfigPath>
</PropertyGroup>
<WriteLinesToFile File="$(_BlazorConfigPath)" Lines="." Overwrite="true" />
<WriteLinesToFile File="$(_BlazorConfigPath)" Lines="$(AssemblyName)/" Overwrite="false" />
<ItemGroup>
<_BlazorPublishConfigContent Include="." />
<_BlazorPublishConfigContent Include="$(AssemblyName)/" />
</ItemGroup>
<WriteLinesToFile
File="$(_BlazorConfigPath)"
Lines="@(_BlazorPublishConfigContent)"
Overwrite="true"
WriteOnlyWhenDifferent="true" />
</Target>
<!-- The following target runs only for standalone publishing -->

View File

@ -0,0 +1,37 @@
<Project>
<PropertyGroup>
<ResolveStaticWebAssetsInputsDependsOn>
$(ResolveStaticWebAssetsInputsDependsOn);
_RemoveBlazorCurrentProjectAssetsFromStaticWebAssets;
</ResolveStaticWebAssetsInputsDependsOn>
<GetCurrentProjectStaticWebAssetsDependsOn>
$(GetCurrentProjectStaticWebAssetsDependsOn);
_RemoveBlazorCurrentProjectAssetsFromStaticWebAssets;
</GetCurrentProjectStaticWebAssetsDependsOn>
</PropertyGroup>
<Target Name="_RemoveBlazorCurrentProjectAssetsFromStaticWebAssets">
<ItemGroup>
<StaticWebAsset Remove="@(StaticWebAsset)" Condition="'%(SourceType)' == ''" />
</ItemGroup>
</Target>
<Target Name="BlazorStaticWebAssetsComputeFilesToPublish"
AfterTargets="_StaticWebAssetsComputeFilesToPublish">
<ItemGroup>
<!-- We need to update the external static web assets to follow the blazor publish output convention that puts them inside $(TargetName)/dist instead of wwwroot -->
<_StandaloneExternalPublishStaticWebAsset Include="@(_ExternalPublishStaticWebAsset)" Condition="'%(RelativePath)' != ''">
<RelativePath>$([MSBuild]::MakeRelative('$(MSBuildProjectDirectory)', '$([MSBuild]::NormalizePath('$([System.Text.RegularExpressions.Regex]::Replace('%(RelativePath)','^wwwroot\\?\/?(.*)','$(BlazorPublishDistDir)$1'))'))'))</RelativePath>
</_StandaloneExternalPublishStaticWebAsset>
<!-- Update doesn't work inside targets so we need to remove the items and re-add them. See https://github.com/microsoft/msbuild/issues/2835 for details -->
<ResolvedFileToPublish Remove="@(_StandaloneExternalPublishStaticWebAsset)" />
<ResolvedFileToPublish Include="@(_StandaloneExternalPublishStaticWebAsset)" />
</ItemGroup>
</Target>
</Project>

View File

@ -0,0 +1,38 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.IO;
using System.Xml.Linq;
using Xunit;
namespace Microsoft.AspNetCore.Blazor.Build
{
public class BlazorCreateRootDescriptorFileTest
{
[Fact]
public void ProducesRootDescriptor()
{
// Arrange/Act
using var stream = new MemoryStream();
// Act
BlazorCreateRootDescriptorFile.WriteRootDescriptor(
stream,
new[] { "MyApp.dll" });
// Assert
stream.Position = 0;
var document = XDocument.Load(stream);
var rootElement = document.Root;
var assemblyElement = Assert.Single(rootElement.Elements());
Assert.Equal("assembly", assemblyElement.Name.ToString());
Assert.Equal("MyApp.dll", assemblyElement.Attribute("fullname").Value);
var typeElement = Assert.Single(assemblyElement.Elements());
Assert.Equal("type", typeElement.Name.ToString());
Assert.Equal("*", typeElement.Attribute("fullname").Value);
Assert.Equal("true", typeElement.Attribute("required").Value);
}
}
}

View File

@ -1,62 +1,41 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Linq;
using System.IO;
using System.Text.Json;
using System.Threading.Tasks;
using Xunit;
namespace Microsoft.AspNetCore.Blazor.Build.Test
namespace Microsoft.AspNetCore.Blazor.Build
{
public class BootJsonWriterTest
{
[Fact]
public void ProducesJsonReferencingAssemblyAndDependencies()
public async Task ProducesJsonReferencingAssemblyAndDependencies()
{
// Arrange/Act
var assemblyReferences = new string[] { "System.Abc.dll", "MyApp.ClassLib.dll", };
var content = BootJsonWriter.GetBootJsonContent(
var assemblyReferences = new string[] { "MyApp.EntryPoint.dll", "System.Abc.dll", "MyApp.ClassLib.dll", };
using var stream = new MemoryStream();
// Act
GenerateBlazorBootJson.WriteBootJson(
stream,
"MyApp.Entrypoint.dll",
"MyNamespace.MyType::MyMethod",
assemblyReferences,
Enumerable.Empty<EmbeddedResourceInfo>(),
linkerEnabled: true);
// Assert
var parsedContent = JsonConvert.DeserializeObject<JObject>(content);
Assert.Equal("MyApp.Entrypoint.dll", parsedContent["main"].Value<string>());
Assert.Equal("MyNamespace.MyType::MyMethod", parsedContent["entryPoint"].Value<string>());
Assert.Equal(assemblyReferences, parsedContent["assemblyReferences"].Values<string>());
}
[Fact]
public void IncludesReferencesToEmbeddedContent()
{
// Arrange/Act
var embeddedContent = new[]
stream.Position = 0;
using var parsedContent = await JsonDocument.ParseAsync(stream);
var rootElement = parsedContent.RootElement;
Assert.Equal("MyApp.Entrypoint.dll", rootElement.GetProperty("entryAssembly").GetString());
var assembliesElement = rootElement.GetProperty("assemblies");
Assert.Equal(assemblyReferences.Length, assembliesElement.GetArrayLength());
for (var i = 0; i < assemblyReferences.Length; i++)
{
new EmbeddedResourceInfo(EmbeddedResourceKind.Static, "my/static/file"),
new EmbeddedResourceInfo(EmbeddedResourceKind.Css, "css/first.css"),
new EmbeddedResourceInfo(EmbeddedResourceKind.JavaScript, "javascript/first.js"),
new EmbeddedResourceInfo(EmbeddedResourceKind.Css, "css/second.css"),
new EmbeddedResourceInfo(EmbeddedResourceKind.JavaScript, "javascript/second.js"),
};
var content = BootJsonWriter.GetBootJsonContent(
"MyApp.Entrypoint",
"MyNamespace.MyType::MyMethod",
assemblyReferences: new[] { "Something.dll" },
embeddedContent: embeddedContent,
linkerEnabled: true);
// Assert
var parsedContent = JsonConvert.DeserializeObject<JObject>(content);
Assert.Equal(
new[] { "css/first.css", "css/second.css" },
parsedContent["cssReferences"].Values<string>());
Assert.Equal(
new[] { "javascript/first.js", "javascript/second.js" },
parsedContent["jsReferences"].Values<string>());
Assert.Equal(assemblyReferences[i], assembliesElement[i].GetString());
}
Assert.True(rootElement.GetProperty("linkerEnabled").GetBoolean());
}
}
}

View File

@ -0,0 +1,950 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Reflection.Metadata;
using System.Reflection.PortableExecutable;
using System.Text;
using System.Text.RegularExpressions;
namespace Microsoft.AspNetCore.Blazor.Build
{
internal class Assert : Xunit.Assert
{
// Matches `{filename}: error {code}: {message} [{project}]
// See https://stackoverflow.com/questions/3441452/msbuild-and-ignorestandarderrorwarningformat/5180353#5180353
private static readonly Regex ErrorRegex = new Regex(@"^(?'location'.+): error (?'errorcode'[A-Z0-9]+): (?'message'.+) \[(?'project'.+)\]$");
private static readonly Regex WarningRegex = new Regex(@"^(?'location'.+): warning (?'errorcode'[A-Z0-9]+): (?'message'.+) \[(?'project'.+)\]$");
private static readonly string[] AllowedBuildWarnings = new[]
{
"MSB3491" , // The process cannot access the file. As long as the build succeeds, we're ok.
"NETSDK1071", // "A PackageReference to 'Microsoft.NETCore.App' specified a Version ..."
};
public static void BuildPassed(MSBuildResult result, bool allowWarnings = false)
{
if (result == null)
{
throw new ArgumentNullException(nameof(result));
}
if (result.ExitCode != 0)
{
throw new BuildFailedException(result);
}
var buildWarnings = GetBuildWarnings(result)
.Where(m => !AllowedBuildWarnings.Contains(m.match.Groups["errorcode"].Value))
.Select(m => m.line);
if (!allowWarnings && buildWarnings.Any())
{
throw new BuildWarningsException(result, buildWarnings);
}
}
public static void BuildError(MSBuildResult result, string errorCode, string location = null)
{
if (result == null)
{
throw new ArgumentNullException(nameof(result));
}
// We don't really need to search line by line, I'm doing this so that it's possible/easy to debug.
var lines = result.Output.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
for (var i = 0; i < lines.Length; i++)
{
var line = lines[i];
var match = ErrorRegex.Match(line);
if (match.Success)
{
if (match.Groups["errorcode"].Value != errorCode)
{
continue;
}
if (location != null && match.Groups["location"].Value.Trim() != location)
{
continue;
}
// This is a match
return;
}
}
throw new BuildErrorMissingException(result, errorCode, location);
}
public static void BuildWarning(MSBuildResult result, string errorCode, string location = null)
{
if (result == null)
{
throw new ArgumentNullException(nameof(result));
}
// We don't really need to search line by line, I'm doing this so that it's possible/easy to debug.
foreach (var (_, match) in GetBuildWarnings(result))
{
if (match.Groups["errorcode"].Value != errorCode)
{
continue;
}
if (location != null && match.Groups["location"].Value.Trim() != location)
{
continue;
}
// This is a match
return;
}
throw new BuildErrorMissingException(result, errorCode, location);
}
private static IEnumerable<(string line, Match match)> GetBuildWarnings(MSBuildResult result)
{
var lines = result.Output.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
for (var i = 0; i < lines.Length; i++)
{
var line = lines[i];
var match = WarningRegex.Match(line);
if (match.Success)
{
yield return (line, match);
}
}
}
public static void BuildFailed(MSBuildResult result)
{
if (result == null)
{
throw new ArgumentNullException(nameof(result));
};
if (result.ExitCode == 0)
{
throw new BuildPassedException(result);
}
}
public static void BuildOutputContainsLine(MSBuildResult result, string match)
{
if (result == null)
{
throw new ArgumentNullException(nameof(result));
}
if (match == null)
{
throw new ArgumentNullException(nameof(match));
}
// We don't really need to search line by line, I'm doing this so that it's possible/easy to debug.
var lines = result.Output.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
for (var i = 0; i < lines.Length; i++)
{
var line = lines[i].Trim();
if (line == match)
{
return;
}
}
throw new BuildOutputMissingException(result, match);
}
public static void BuildOutputDoesNotContainLine(MSBuildResult result, string match)
{
if (result == null)
{
throw new ArgumentNullException(nameof(result));
}
if (match == null)
{
throw new ArgumentNullException(nameof(match));
}
// We don't really need to search line by line, I'm doing this so that it's possible/easy to debug.
var lines = result.Output.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
for (var i = 0; i < lines.Length; i++)
{
var line = lines[i].Trim();
if (line == match)
{
throw new BuildOutputContainsLineException(result, match);
}
}
}
public static void FileContains(MSBuildResult result, string filePath, string match)
{
if (result == null)
{
throw new ArgumentNullException(nameof(result));
}
filePath = Path.Combine(result.Project.DirectoryPath, filePath);
FileExists(result, filePath);
var text = File.ReadAllText(filePath);
if (text.Contains(match))
{
return;
}
throw new FileContentMissingException(result, filePath, File.ReadAllText(filePath), match);
}
public static void FileDoesNotContain(MSBuildResult result, string filePath, string match)
{
if (result == null)
{
throw new ArgumentNullException(nameof(result));
}
filePath = Path.Combine(result.Project.DirectoryPath, filePath);
FileExists(result, filePath);
var text = File.ReadAllText(filePath);
if (text.Contains(match))
{
throw new FileContentFoundException(result, filePath, File.ReadAllText(filePath), match);
}
}
public static void FileContentEquals(MSBuildResult result, string filePath, string expected)
{
if (result == null)
{
throw new ArgumentNullException(nameof(result));
}
filePath = Path.Combine(result.Project.DirectoryPath, filePath);
FileExists(result, filePath);
var actual = File.ReadAllText(filePath);
if (!actual.Equals(expected, StringComparison.Ordinal))
{
throw new FileContentNotEqualException(result, filePath, expected, actual);
}
}
public static void FileEquals(MSBuildResult result, string expected, string actual)
{
if (result == null)
{
throw new ArgumentNullException(nameof(result));
}
expected = Path.Combine(result.Project.DirectoryPath, expected);
actual = Path.Combine(result.Project.DirectoryPath, actual);
FileExists(result, expected);
FileExists(result, actual);
if (!Enumerable.SequenceEqual(File.ReadAllBytes(expected), File.ReadAllBytes(actual)))
{
throw new FilesNotEqualException(result, expected, actual);
}
}
public static void FileContainsLine(MSBuildResult result, string filePath, string match)
{
if (result == null)
{
throw new ArgumentNullException(nameof(result));
}
filePath = Path.Combine(result.Project.DirectoryPath, filePath);
FileExists(result, filePath);
var lines = File.ReadAllLines(filePath);
for (var i = 0; i < lines.Length; i++)
{
var line = lines[i].Trim();
if (line == match)
{
return;
}
}
throw new FileContentMissingException(result, filePath, File.ReadAllText(filePath), match);
}
public static void FileDoesNotContainLine(MSBuildResult result, string filePath, string match)
{
if (result == null)
{
throw new ArgumentNullException(nameof(result));
}
filePath = Path.Combine(result.Project.DirectoryPath, filePath);
FileExists(result, filePath);
var lines = File.ReadAllLines(filePath);
for (var i = 0; i < lines.Length; i++)
{
var line = lines[i].Trim();
if (line == match)
{
throw new FileContentFoundException(result, filePath, File.ReadAllText(filePath), match);
}
}
}
public static string FileExists(MSBuildResult result, params string[] paths)
{
if (result == null)
{
throw new ArgumentNullException(nameof(result));
}
var filePath = Path.Combine(result.Project.DirectoryPath, Path.Combine(paths));
if (!File.Exists(filePath))
{
throw new FileMissingException(result, filePath);
}
return filePath;
}
public static void FileCountEquals(MSBuildResult result, int expected, string directoryPath, string searchPattern)
{
if (result == null)
{
throw new ArgumentNullException(nameof(result));
}
if (directoryPath == null)
{
throw new ArgumentNullException(nameof(directoryPath));
}
if (searchPattern == null)
{
throw new ArgumentNullException(nameof(searchPattern));
}
directoryPath = Path.Combine(result.Project.DirectoryPath, directoryPath);
if (Directory.Exists(directoryPath))
{
var files = Directory.GetFiles(directoryPath, searchPattern, SearchOption.AllDirectories);
if (files.Length != expected)
{
throw new FileCountException(result, expected, directoryPath, searchPattern, files);
}
}
else if (expected > 0)
{
// directory doesn't exist, that's OK if we expected to find nothing.
throw new FileCountException(result, expected, directoryPath, searchPattern, Array.Empty<string>());
}
}
public static void FileDoesNotExist(MSBuildResult result, params string[] paths)
{
if (result == null)
{
throw new ArgumentNullException(nameof(result));
}
var filePath = Path.Combine(result.Project.DirectoryPath, Path.Combine(paths));
if (File.Exists(filePath))
{
throw new FileFoundException(result, filePath);
}
}
public static void NuspecContains(MSBuildResult result, string nuspecPath, string expected)
{
if (result == null)
{
throw new ArgumentNullException(nameof(result));
}
if (nuspecPath == null)
{
throw new ArgumentNullException(nameof(nuspecPath));
}
if (expected == null)
{
throw new ArgumentNullException(nameof(expected));
}
nuspecPath = Path.Combine(result.Project.DirectoryPath, nuspecPath);
FileExists(result, nuspecPath);
var content = File.ReadAllText(nuspecPath);
if (!content.Contains(expected))
{
throw new NuspecException(result, nuspecPath, content, expected);
}
}
public static void NuspecDoesNotContain(MSBuildResult result, string nuspecPath, string expected)
{
if (result == null)
{
throw new ArgumentNullException(nameof(result));
}
if (nuspecPath == null)
{
throw new ArgumentNullException(nameof(nuspecPath));
}
if (expected == null)
{
throw new ArgumentNullException(nameof(expected));
}
nuspecPath = Path.Combine(result.Project.DirectoryPath, nuspecPath);
FileExists(result, nuspecPath);
var content = File.ReadAllText(nuspecPath);
if (content.Contains(expected))
{
throw new NuspecFoundException(result, nuspecPath, content, expected);
}
}
// This method extracts the nupkg to a fixed directory path. To avoid the extra work of
// cleaning up after each invocation, this method accepts multiple files.
public static void NupkgContains(MSBuildResult result, string nupkgPath, params string[] filePaths)
{
if (result == null)
{
throw new ArgumentNullException(nameof(result));
}
if (nupkgPath == null)
{
throw new ArgumentNullException(nameof(nupkgPath));
}
if (filePaths == null)
{
throw new ArgumentNullException(nameof(filePaths));
}
nupkgPath = Path.Combine(result.Project.DirectoryPath, nupkgPath);
FileExists(result, nupkgPath);
var unzipped = Path.Combine(result.Project.DirectoryPath, Path.GetFileNameWithoutExtension(nupkgPath));
ZipFile.ExtractToDirectory(nupkgPath, unzipped);
foreach (var filePath in filePaths)
{
if (!File.Exists(Path.Combine(unzipped, filePath)))
{
throw new NupkgFileMissingException(result, nupkgPath, filePath);
}
}
}
// This method extracts the nupkg to a fixed directory path. To avoid the extra work of
// cleaning up after each invocation, this method accepts multiple files.
public static void NupkgDoesNotContain(MSBuildResult result, string nupkgPath, params string[] filePaths)
{
if (result == null)
{
throw new ArgumentNullException(nameof(result));
}
if (nupkgPath == null)
{
throw new ArgumentNullException(nameof(nupkgPath));
}
if (filePaths == null)
{
throw new ArgumentNullException(nameof(filePaths));
}
nupkgPath = Path.Combine(result.Project.DirectoryPath, nupkgPath);
FileExists(result, nupkgPath);
var unzipped = Path.Combine(result.Project.DirectoryPath, Path.GetFileNameWithoutExtension(nupkgPath));
ZipFile.ExtractToDirectory(nupkgPath, unzipped);
foreach (var filePath in filePaths)
{
if (File.Exists(Path.Combine(unzipped, filePath)))
{
throw new NupkgFileFoundException(result, nupkgPath, filePath);
}
}
}
public static void AssemblyContainsType(MSBuildResult result, string assemblyPath, string fullTypeName)
{
if (result == null)
{
throw new ArgumentNullException(nameof(result));
}
assemblyPath = Path.Combine(result.Project.DirectoryPath, Path.Combine(assemblyPath));
var typeNames = GetDeclaredTypeNames(assemblyPath);
Assert.Contains(fullTypeName, typeNames);
}
public static void AssemblyDoesNotContainType(MSBuildResult result, string assemblyPath, string fullTypeName)
{
if (result == null)
{
throw new ArgumentNullException(nameof(result));
}
assemblyPath = Path.Combine(result.Project.DirectoryPath, Path.Combine(assemblyPath));
var typeNames = GetDeclaredTypeNames(assemblyPath);
Assert.DoesNotContain(fullTypeName, typeNames);
}
private static IEnumerable<string> GetDeclaredTypeNames(string assemblyPath)
{
using (var file = File.OpenRead(assemblyPath))
{
var peReader = new PEReader(file);
var metadataReader = peReader.GetMetadataReader();
return metadataReader.TypeDefinitions.Where(t => !t.IsNil).Select(t =>
{
var type = metadataReader.GetTypeDefinition(t);
return metadataReader.GetString(type.Namespace) + "." + metadataReader.GetString(type.Name);
}).ToArray();
}
}
public static void AssemblyHasAttribute(MSBuildResult result, string assemblyPath, string fullTypeName)
{
if (result == null)
{
throw new ArgumentNullException(nameof(result));
}
assemblyPath = Path.Combine(result.Project.DirectoryPath, Path.Combine(assemblyPath));
var typeNames = GetAssemblyAttributes(assemblyPath);
Assert.Contains(fullTypeName, typeNames);
}
private static IEnumerable<string> GetAssemblyAttributes(string assemblyPath)
{
using (var file = File.OpenRead(assemblyPath))
{
var peReader = new PEReader(file);
var metadataReader = peReader.GetMetadataReader();
return metadataReader.CustomAttributes.Where(t => !t.IsNil).Select(t =>
{
var attribute = metadataReader.GetCustomAttribute(t);
var constructor = metadataReader.GetMemberReference((MemberReferenceHandle)attribute.Constructor);
var type = metadataReader.GetTypeReference((TypeReferenceHandle)constructor.Parent);
return metadataReader.GetString(type.Namespace) + "." + metadataReader.GetString(type.Name);
}).ToArray();
}
}
private abstract class MSBuildXunitException : Xunit.Sdk.XunitException
{
protected MSBuildXunitException(MSBuildResult result)
{
Result = result;
}
protected abstract string Heading { get; }
public MSBuildResult Result { get; }
public override string Message
{
get
{
var message = new StringBuilder();
message.AppendLine(Heading);
message.Append(Result.FileName);
message.Append(" ");
message.Append(Result.Arguments);
message.AppendLine();
message.AppendLine();
message.Append(Result.Output);
message.AppendLine();
message.Append("Exit Code:");
message.Append(Result.ExitCode);
return message.ToString();
}
}
}
private class BuildErrorMissingException : MSBuildXunitException
{
public BuildErrorMissingException(MSBuildResult result, string errorCode, string location)
: base(result)
{
ErrorCode = errorCode;
Location = location;
}
public string ErrorCode { get; }
public string Location { get; }
protected override string Heading
{
get
{
return
$"Error code '{ErrorCode}' was not found." + Environment.NewLine +
$"Looking for '{Location ?? ".*"}: error {ErrorCode}: .*'";
}
}
}
private class BuildFailedException : MSBuildXunitException
{
public BuildFailedException(MSBuildResult result)
: base(result)
{
}
protected override string Heading => "Build failed.";
}
private class BuildWarningsException : MSBuildXunitException
{
public BuildWarningsException(MSBuildResult result, IEnumerable<string> warnings)
: base(result)
{
Warnings = warnings.ToList();
}
public List<string> Warnings { get; }
protected override string Heading => "Build contains unexpected warnings: " + string.Join(Environment.NewLine, Warnings);
}
private class BuildPassedException : MSBuildXunitException
{
public BuildPassedException(MSBuildResult result)
: base(result)
{
}
protected override string Heading => "Build should have failed, but it passed.";
}
private class BuildOutputMissingException : MSBuildXunitException
{
public BuildOutputMissingException(MSBuildResult result, string match)
: base(result)
{
Match = match;
}
public string Match { get; }
protected override string Heading => $"Build did not contain the line: '{Match}'.";
}
private class BuildOutputContainsLineException : MSBuildXunitException
{
public BuildOutputContainsLineException(MSBuildResult result, string match)
: base(result)
{
Match = match;
}
public string Match { get; }
protected override string Heading => $"Build output contains the line: '{Match}'.";
}
private class FileContentFoundException : MSBuildXunitException
{
public FileContentFoundException(MSBuildResult result, string filePath, string content, string match)
: base(result)
{
FilePath = filePath;
Content = content;
Match = match;
}
public string Content { get; }
public string FilePath { get; }
public string Match { get; }
protected override string Heading
{
get
{
var builder = new StringBuilder();
builder.AppendFormat("File content of '{0}' should not contain line: '{1}'.", FilePath, Match);
builder.AppendLine();
builder.AppendLine();
builder.AppendLine(Content);
return builder.ToString();
}
}
}
private class FileContentMissingException : MSBuildXunitException
{
public FileContentMissingException(MSBuildResult result, string filePath, string content, string match)
: base(result)
{
FilePath = filePath;
Content = content;
Match = match;
}
public string Content { get; }
public string FilePath { get; }
public string Match { get; }
protected override string Heading
{
get
{
var builder = new StringBuilder();
builder.AppendFormat("File content of '{0}' did not contain the line: '{1}'.", FilePath, Match);
builder.AppendLine();
builder.AppendLine();
builder.AppendLine(Content);
return builder.ToString();
}
}
}
private class FileContentNotEqualException : MSBuildXunitException
{
public FileContentNotEqualException(MSBuildResult result, string filePath, string expected, string actual)
: base(result)
{
FilePath = filePath;
Expected = expected;
Actual = actual;
}
public string Actual { get; }
public string FilePath { get; }
public string Expected { get; }
protected override string Heading
{
get
{
var builder = new StringBuilder();
builder.AppendFormat("File content of '{0}' did not match the expected content: '{1}'.", FilePath, Expected);
builder.AppendLine();
builder.AppendLine();
builder.AppendLine(Actual);
return builder.ToString();
}
}
}
private class FilesNotEqualException : MSBuildXunitException
{
public FilesNotEqualException(MSBuildResult result, string expected, string actual)
: base(result)
{
Expected = expected;
Actual = actual;
}
public string Actual { get; }
public string Expected { get; }
protected override string Heading
{
get
{
var builder = new StringBuilder();
builder.AppendFormat("File content of '{0}' did not match the contents of '{1}'.", Expected, Actual);
builder.AppendLine();
builder.AppendLine();
builder.AppendLine(Actual);
return builder.ToString();
}
}
}
private class FileMissingException : MSBuildXunitException
{
public FileMissingException(MSBuildResult result, string filePath)
: base(result)
{
FilePath = filePath;
}
public string FilePath { get; }
protected override string Heading => $"File: '{FilePath}' was not found.";
}
private class FileCountException : MSBuildXunitException
{
public FileCountException(MSBuildResult result, int expected, string directoryPath, string searchPattern, string[] files)
: base(result)
{
Expected = expected;
DirectoryPath = directoryPath;
SearchPattern = searchPattern;
Files = files;
}
public string DirectoryPath { get; }
public int Expected { get; }
public string[] Files { get; }
public string SearchPattern { get; }
protected override string Heading
{
get
{
var heading = new StringBuilder();
heading.AppendLine($"Expected {Expected} files matching {SearchPattern} in {DirectoryPath}, found {Files.Length}.");
if (Files.Length > 0)
{
heading.AppendLine("Files:");
foreach (var file in Files)
{
heading.Append("\t");
heading.Append(file);
}
heading.AppendLine();
}
return heading.ToString();
}
}
}
private class FileFoundException : MSBuildXunitException
{
public FileFoundException(MSBuildResult result, string filePath)
: base(result)
{
FilePath = filePath;
}
public string FilePath { get; }
protected override string Heading => $"File: '{FilePath}' was found, but should not exist.";
}
private class NuspecException : MSBuildXunitException
{
public NuspecException(MSBuildResult result, string filePath, string content, string expected)
: base(result)
{
FilePath = filePath;
Content = content;
Expected = expected;
}
public string Content { get; }
public string Expected { get; }
public string FilePath { get; }
protected override string Heading
{
get
{
return
$"nuspec: '{FilePath}' did not contain the expected content." + Environment.NewLine +
Environment.NewLine +
$"expected: {Expected}" + Environment.NewLine +
Environment.NewLine +
$"actual: {Content}";
}
}
}
private class NuspecFoundException : MSBuildXunitException
{
public NuspecFoundException(MSBuildResult result, string filePath, string content, string expected)
: base(result)
{
FilePath = filePath;
Content = content;
Expected = expected;
}
public string Content { get; }
public string Expected { get; }
public string FilePath { get; }
protected override string Heading
{
get
{
return
$"nuspec: '{FilePath}' should not contain the content {Expected}." +
Environment.NewLine +
$"actual content: {Content}";
}
}
}
private class NupkgFileMissingException : MSBuildXunitException
{
public NupkgFileMissingException(MSBuildResult result, string nupkgPath, string filePath)
: base(result)
{
NupkgPath = nupkgPath;
FilePath = filePath;
}
public string FilePath { get; }
public string NupkgPath { get; }
protected override string Heading => $"File: '{FilePath}' was not found was not found in {NupkgPath}.";
}
private class NupkgFileFoundException : MSBuildXunitException
{
public NupkgFileFoundException(MSBuildResult result, string nupkgPath, string filePath)
: base(result)
{
NupkgPath = nupkgPath;
FilePath = filePath;
}
public string FilePath { get; }
public string NupkgPath { get; }
protected override string Heading => $"File: '{FilePath}' was found in {NupkgPath}.";
}
}
}

View File

@ -0,0 +1,40 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Threading.Tasks;
using Xunit;
namespace Microsoft.AspNetCore.Blazor.Build
{
public class BuildIncrementalismTest
{
[Fact]
public async Task Build_WithLinker_IsIncremental()
{
// Arrange
using var project = ProjectDirectory.Create("standalone");
var result = await MSBuildProcessManager.DotnetMSBuild(project);
Assert.BuildPassed(result);
var buildOutputDirectory = project.BuildOutputDirectory;
// Act
var thumbPrint = FileThumbPrint.CreateFolderThumbprint(project, project.BuildOutputDirectory);
// Assert
for (var i = 0; i < 3; i++)
{
result = await MSBuildProcessManager.DotnetMSBuild(project);
Assert.BuildPassed(result);
var newThumbPrint = FileThumbPrint.CreateFolderThumbprint(project, project.BuildOutputDirectory);
Assert.Equal(thumbPrint.Count, newThumbPrint.Count);
for (var j = 0; j < thumbPrint.Count; j++)
{
Assert.Equal(thumbPrint[j], newThumbPrint[j]);
}
}
}
}
}

View File

@ -0,0 +1,135 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.IO;
using System.Threading.Tasks;
using Xunit;
namespace Microsoft.AspNetCore.Blazor.Build
{
public class BuildIntegrationTest
{
[Fact]
public async Task Build_WithDefaultSettings_Works()
{
// Arrange
using var project = ProjectDirectory.Create("standalone");
var result = await MSBuildProcessManager.DotnetMSBuild(project);
Assert.BuildPassed(result);
var buildOutputDirectory = project.BuildOutputDirectory;
Assert.FileExists(result, buildOutputDirectory, "dist", "_framework", "blazor.boot.json");
Assert.FileExists(result, buildOutputDirectory, "dist", "_framework", "blazor.webassembly.js");
Assert.FileExists(result, buildOutputDirectory, "dist", "_framework", "wasm", "dotnet.wasm");
Assert.FileExists(result, buildOutputDirectory, "dist", "_framework", "wasm", "dotnet.js");
Assert.FileExists(result, buildOutputDirectory, "dist", "_framework", "_bin", "standalone.dll");
Assert.FileExists(result, buildOutputDirectory, "dist", "_framework", "_bin", "Microsoft.Extensions.Logging.Abstractions.dll"); // Verify dependencies are part of the output.
}
[Fact]
public async Task Build_Hosted_Works()
{
// Arrange
using var project = ProjectDirectory.Create("blazorhosted", additionalProjects: new[] { "standalone", "razorclasslibrary", });
project.TargetFramework = "netcoreapp3.1";
var result = await MSBuildProcessManager.DotnetMSBuild(project);
Assert.BuildPassed(result);
var buildOutputDirectory = project.BuildOutputDirectory;
var blazorConfig = Path.Combine(buildOutputDirectory, "standalone.blazor.config");
Assert.FileExists(result, blazorConfig);
var path = Path.GetFullPath(Path.Combine(project.SolutionPath, "standalone", "bin", project.Configuration, "netstandard2.1", "standalone.dll"));
Assert.FileContains(result, blazorConfig, path);
Assert.FileDoesNotExist(result, buildOutputDirectory, "dist", "_framework", "_bin", "standalone.dll");
}
[Fact]
public async Task Build_WithLinkOnBuildDisabled_Works()
{
// Arrange
using var project = ProjectDirectory.Create("standalone");
project.AddProjectFileContent(
@"<PropertyGroup>
<BlazorLinkOnBuild>false</BlazorLinkOnBuild>
</PropertyGroup>");
var result = await MSBuildProcessManager.DotnetMSBuild(project);
Assert.BuildPassed(result);
var buildOutputDirectory = project.BuildOutputDirectory;
Assert.FileExists(result, buildOutputDirectory, "dist", "_framework", "blazor.boot.json");
Assert.FileExists(result, buildOutputDirectory, "dist", "_framework", "blazor.webassembly.js");
Assert.FileExists(result, buildOutputDirectory, "dist", "_framework", "wasm", "dotnet.wasm");
Assert.FileExists(result, buildOutputDirectory, "dist", "_framework", "wasm", "dotnet.js");
Assert.FileExists(result, buildOutputDirectory, "dist", "_framework", "_bin", "standalone.dll");
Assert.FileExists(result, buildOutputDirectory, "dist", "_framework", "_bin", "Microsoft.Extensions.Logging.Abstractions.dll"); // Verify dependencies are part of the output.
}
[Fact]
public async Task Build_SatelliteAssembliesAreCopiedToBuildOutput()
{
// Arrange
using var project = ProjectDirectory.Create("standalone", additionalProjects: new[] { "razorclasslibrary", "classlibrarywithsatelliteassemblies" });
project.AddProjectFileContent(
@"
<PropertyGroup>
<DefineConstants>$(DefineConstants);REFERENCE_classlibrarywithsatelliteassemblies</DefineConstants>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include=""..\classlibrarywithsatelliteassemblies\classlibrarywithsatelliteassemblies.csproj"" />
</ItemGroup>");
var result = await MSBuildProcessManager.DotnetMSBuild(project, args: "/restore");
Assert.BuildPassed(result);
var buildOutputDirectory = project.BuildOutputDirectory;
Assert.FileExists(result, buildOutputDirectory, "dist", "_framework", "_bin", "standalone.dll");
Assert.FileExists(result, buildOutputDirectory, "dist", "_framework", "_bin", "classlibrarywithsatelliteassemblies.dll");
Assert.FileExists(result, buildOutputDirectory, "dist", "_framework", "_bin", "Microsoft.CodeAnalysis.CSharp.dll");
Assert.FileExists(result, buildOutputDirectory, "dist", "_framework", "_bin", "fr", "Microsoft.CodeAnalysis.CSharp.resources.dll"); // Verify satellite assemblies are present in the build output.
var bootJsonPath = Path.Combine(buildOutputDirectory, "dist", "_framework", "blazor.boot.json");
Assert.FileContains(result, bootJsonPath, "\"Microsoft.CodeAnalysis.CSharp.dll\"");
Assert.FileContains(result, bootJsonPath, "\"fr\\/Microsoft.CodeAnalysis.CSharp.resources.dll\"");
}
[Fact]
public async Task Build_WithBlazorLinkOnBuildFalse_SatelliteAssembliesAreCopiedToBuildOutput()
{
// Arrange
using var project = ProjectDirectory.Create("standalone", additionalProjects: new[] { "razorclasslibrary", "classlibrarywithsatelliteassemblies" });
project.AddProjectFileContent(
@"
<PropertyGroup>
<BlazorLinkOnBuild>false</BlazorLinkOnBuild>
<DefineConstants>$(DefineConstants);REFERENCE_classlibrarywithsatelliteassemblies</DefineConstants>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include=""..\classlibrarywithsatelliteassemblies\classlibrarywithsatelliteassemblies.csproj"" />
</ItemGroup>");
var result = await MSBuildProcessManager.DotnetMSBuild(project, args: "/restore");
Assert.BuildPassed(result);
var buildOutputDirectory = project.BuildOutputDirectory;
Assert.FileExists(result, buildOutputDirectory, "dist", "_framework", "_bin", "standalone.dll");
Assert.FileExists(result, buildOutputDirectory, "dist", "_framework", "_bin", "classlibrarywithsatelliteassemblies.dll");
Assert.FileExists(result, buildOutputDirectory, "dist", "_framework", "_bin", "Microsoft.CodeAnalysis.CSharp.dll");
Assert.FileExists(result, buildOutputDirectory, "dist", "_framework", "_bin", "fr", "Microsoft.CodeAnalysis.CSharp.resources.dll"); // Verify satellite assemblies are present in the build output.
var bootJsonPath = Path.Combine(buildOutputDirectory, "dist", "_framework", "blazor.boot.json");
Assert.FileContains(result, bootJsonPath, "\"Microsoft.CodeAnalysis.CSharp.dll\"");
Assert.FileContains(result, bootJsonPath, "\"fr\\/Microsoft.CodeAnalysis.CSharp.resources.dll\"");
}
}
}

View File

@ -0,0 +1,74 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
namespace Microsoft.AspNetCore.Blazor.Build
{
internal class FileThumbPrint : IEquatable<FileThumbPrint>
{
private FileThumbPrint(string path, DateTime lastWriteTimeUtc, string hash)
{
FilePath = path;
LastWriteTimeUtc = lastWriteTimeUtc;
Hash = hash;
}
public string FilePath { get; }
public DateTime LastWriteTimeUtc { get; }
public string Hash { get; }
public override string ToString()
{
return $"{Path.GetFileName(FilePath)}, {LastWriteTimeUtc.ToString("u")}, {Hash}";
}
/// <summary>
/// Returns a list of thumbprints for all files (recursive) in the specified directory, sorted by file paths.
/// </summary>
public static List<FileThumbPrint> CreateFolderThumbprint(ProjectDirectory project, string directoryPath, params string[] filesToIgnore)
{
directoryPath = Path.Combine(project.DirectoryPath, directoryPath);
var files = Directory.GetFiles(directoryPath).Where(p => !filesToIgnore.Contains(p));
var thumbprintLookup = new List<FileThumbPrint>();
foreach (var file in files)
{
var thumbprint = Create(file);
thumbprintLookup.Add(thumbprint);
}
thumbprintLookup.Sort(Comparer<FileThumbPrint>.Create((a, b) => StringComparer.Ordinal.Compare(a.FilePath, b.FilePath)));
return thumbprintLookup;
}
public static FileThumbPrint Create(string path)
{
byte[] hashBytes;
using (var sha1 = SHA1.Create())
using (var fileStream = File.OpenRead(path))
{
hashBytes = sha1.ComputeHash(fileStream);
}
var hash = Convert.ToBase64String(hashBytes);
var lastWriteTimeUtc = File.GetLastWriteTimeUtc(path);
return new FileThumbPrint(path, lastWriteTimeUtc, hash);
}
public bool Equals(FileThumbPrint other)
{
return
string.Equals(FilePath, other.FilePath, StringComparison.Ordinal) &&
LastWriteTimeUtc == other.LastWriteTimeUtc &&
string.Equals(Hash, other.Hash, StringComparison.Ordinal);
}
public override int GetHashCode() => LastWriteTimeUtc.GetHashCode();
}
}

View File

@ -0,0 +1,282 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.CommandLineUtils;
namespace Microsoft.AspNetCore.Blazor.Build
{
internal static class MSBuildProcessManager
{
public static Task<MSBuildResult> DotnetMSBuild(
ProjectDirectory project,
string target = "Build",
string args = null)
{
var buildArgumentList = new List<string>
{
// Disable node-reuse. We don't want msbuild processes to stick around
// once the test is completed.
"/nr:false",
// Always generate a bin log for debugging purposes
"/bl",
};
buildArgumentList.Add($"/t:{target}");
buildArgumentList.Add($"/p:Configuration={project.Configuration}");
buildArgumentList.Add(args);
var buildArguments = string.Join(" ", buildArgumentList);
return MSBuildProcessManager.RunProcessAsync(project, buildArguments);
}
public static async Task<MSBuildResult> RunProcessAsync(
ProjectDirectory project,
string arguments,
TimeSpan? timeout = null)
{
var processStartInfo = new ProcessStartInfo()
{
WorkingDirectory = project.DirectoryPath,
UseShellExecute = false,
RedirectStandardError = true,
RedirectStandardOutput = true,
};
processStartInfo.FileName = DotNetMuxer.MuxerPathOrDefault();
processStartInfo.Arguments = $"msbuild {arguments}";
// Suppresses the 'Welcome to .NET Core!' output that times out tests and causes locked file issues.
// When using dotnet we're not guarunteed to run in an environment where the dotnet.exe has had its first run experience already invoked.
processStartInfo.EnvironmentVariables["DOTNET_SKIP_FIRST_TIME_EXPERIENCE"] = "true";
var processResult = await RunProcessCoreAsync(processStartInfo, timeout);
return new MSBuildResult(project, processResult.FileName, processResult.Arguments, processResult.ExitCode, processResult.Output);
}
internal static Task<ProcessResult> RunProcessCoreAsync(
ProcessStartInfo processStartInfo,
TimeSpan? timeout = null)
{
timeout = timeout ?? TimeSpan.FromSeconds(5 * 60);
var process = new Process()
{
StartInfo = processStartInfo,
EnableRaisingEvents = true,
};
var output = new StringBuilder();
var outputLock = new object();
var diagnostics = new StringBuilder();
diagnostics.AppendLine("Process execution diagnostics:");
process.ErrorDataReceived += Process_ErrorDataReceived;
process.OutputDataReceived += Process_OutputDataReceived;
process.Start();
process.BeginOutputReadLine();
process.BeginErrorReadLine();
var timeoutTask = GetTimeoutForProcess(process, timeout, diagnostics);
var waitTask = Task.Run(() =>
{
// We need to use two WaitForExit calls to ensure that all of the output/events are processed. Previously
// this code used Process.Exited, which could result in us missing some output due to the ordering of
// events.
//
// See the remarks here: https://msdn.microsoft.com/en-us/library/ty0d8k56(v=vs.110).aspx
if (!process.WaitForExit(Int32.MaxValue))
{
// unreachable - the timeoutTask will kill the process before this happens.
throw new TimeoutException();
}
process.WaitForExit();
string outputString;
lock (outputLock)
{
// This marks the end of the diagnostic info which we collect when something goes wrong.
diagnostics.AppendLine("Process output:");
// Expected output
// Process execution diagnostics:
// ...
// Process output:
outputString = diagnostics.ToString();
outputString += output.ToString();
}
var result = new ProcessResult(process.StartInfo.FileName, process.StartInfo.Arguments, process.ExitCode, outputString);
return result;
});
return Task.WhenAny<ProcessResult>(waitTask, timeoutTask).Unwrap();
void Process_ErrorDataReceived(object sender, DataReceivedEventArgs e)
{
lock (outputLock)
{
output.AppendLine(e.Data);
}
}
void Process_OutputDataReceived(object sender, DataReceivedEventArgs e)
{
lock (outputLock)
{
output.AppendLine(e.Data);
}
}
async Task<ProcessResult> GetTimeoutForProcess(Process process, TimeSpan? timeout, StringBuilder diagnostics)
{
await Task.Delay(timeout.Value);
// Don't timeout during debug sessions
while (Debugger.IsAttached)
{
Thread.Sleep(TimeSpan.FromSeconds(1));
}
if (!process.HasExited)
{
await CollectDumps(process, timeout, diagnostics);
// This is a timeout.
process.Kill();
}
throw new TimeoutException($"command '${process.StartInfo.FileName} {process.StartInfo.Arguments}' timed out after {timeout}. Output: {output.ToString()}");
}
static async Task CollectDumps(Process process, TimeSpan? timeout, StringBuilder diagnostics)
{
var procDumpProcess = await CaptureDump(process, timeout, diagnostics);
var allDotNetProcesses = Process.GetProcessesByName("dotnet");
var allDotNetChildProcessCandidates = allDotNetProcesses
.Where(p => p.StartTime >= process.StartTime && p.Id != process.Id);
if (!allDotNetChildProcessCandidates.Any())
{
diagnostics.AppendLine("Couldn't find any candidate child process.");
foreach (var dotnetProcess in allDotNetProcesses)
{
diagnostics.AppendLine($"Found dotnet process with PID {dotnetProcess.Id} and start time {dotnetProcess.StartTime}.");
}
}
foreach (var childProcess in allDotNetChildProcessCandidates)
{
diagnostics.AppendLine($"Found child process candidate '{childProcess.Id}'.");
}
var childrenProcessDumpProcesses = await Task.WhenAll(allDotNetChildProcessCandidates.Select(d => CaptureDump(d, timeout, diagnostics)));
foreach (var childProcess in childrenProcessDumpProcesses)
{
if (childProcess != null && childProcess.HasExited)
{
diagnostics.AppendLine($"ProcDump failed to run for child dotnet process candidate '{process.Id}'.");
childProcess.Kill();
}
}
if (procDumpProcess != null && procDumpProcess.HasExited)
{
diagnostics.AppendLine($"ProcDump failed to run for '{process.Id}'.");
procDumpProcess.Kill();
}
}
static async Task<Process> CaptureDump(Process process, TimeSpan? timeout, StringBuilder diagnostics)
{
var metadataAttributes = Assembly.GetExecutingAssembly()
.GetCustomAttributes<AssemblyMetadataAttribute>();
var procDumpPath = metadataAttributes
.SingleOrDefault(ama => ama.Key == "ProcDumpToolPath")?.Value;
if (string.IsNullOrEmpty(procDumpPath))
{
diagnostics.AppendLine("ProcDumpPath not defined.");
return null;
}
var procDumpExePath = Path.Combine(procDumpPath, "procdump.exe");
if (!File.Exists(procDumpExePath))
{
diagnostics.AppendLine($"Can't find procdump.exe in '{procDumpPath}'.");
return null;
}
var dumpDirectory = metadataAttributes
.SingleOrDefault(ama => ama.Key == "ArtifactsLogDir")?.Value;
if (string.IsNullOrEmpty(dumpDirectory))
{
diagnostics.AppendLine("ArtifactsLogDir not defined.");
return null;
}
if (!Directory.Exists(dumpDirectory))
{
diagnostics.AppendLine($"'{dumpDirectory}' does not exist.");
return null;
}
var procDumpPattern = Path.Combine(dumpDirectory, "HangingProcess_PROCESSNAME_PID_YYMMDD_HHMMSS.dmp");
var procDumpStartInfo = new ProcessStartInfo(
procDumpExePath,
$"-accepteula -ma {process.Id} {procDumpPattern}")
{
CreateNoWindow = true,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true
};
var procDumpProcess = Process.Start(procDumpStartInfo);
var tcs = new TaskCompletionSource<object>();
procDumpProcess.Exited += (s, a) => tcs.TrySetResult(null);
procDumpProcess.EnableRaisingEvents = true;
await Task.WhenAny(tcs.Task, Task.Delay(timeout ?? TimeSpan.FromSeconds(30)));
return procDumpProcess;
}
}
internal class ProcessResult
{
public ProcessResult(string fileName, string arguments, int exitCode, string output)
{
FileName = fileName;
Arguments = arguments;
ExitCode = exitCode;
Output = output;
}
public string Arguments { get; }
public string FileName { get; }
public int ExitCode { get; }
public string Output { get; }
}
}
}

View File

@ -0,0 +1,28 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNetCore.Blazor.Build
{
internal class MSBuildResult
{
public MSBuildResult(ProjectDirectory project, string fileName, string arguments, int exitCode, string output)
{
Project = project;
FileName = fileName;
Arguments = arguments;
ExitCode = exitCode;
Output = output;
}
public ProjectDirectory Project { get; }
public string Arguments { get; }
public string FileName { get; }
public int ExitCode { get; }
public string Output { get; }
}
}

View File

@ -0,0 +1,211 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
namespace Microsoft.AspNetCore.Blazor.Build
{
internal class ProjectDirectory : IDisposable
{
public bool PreserveWorkingDirectory { get; set; } = false;
private static readonly string RepoRoot = GetTestAttribute("Testing.RepoRoot");
public static ProjectDirectory Create(string projectName, string baseDirectory = "", string[] additionalProjects = null)
{
var destinationPath = Path.Combine(Path.GetTempPath(), "BlazorBuild", baseDirectory, Path.GetRandomFileName());
Directory.CreateDirectory(destinationPath);
try
{
if (Directory.EnumerateFiles(destinationPath).Any())
{
throw new InvalidOperationException($"{destinationPath} should be empty");
}
if (string.IsNullOrEmpty(RepoRoot))
{
throw new InvalidOperationException("RepoRoot was not specified.");
}
var testAppsRoot = Path.Combine(RepoRoot, "src", "Components", "Blazor", "Build", "testassets");
foreach (var project in new string[] { projectName, }.Concat(additionalProjects ?? Array.Empty<string>()))
{
var projectRoot = Path.Combine(testAppsRoot, project);
if (!Directory.Exists(projectRoot))
{
throw new InvalidOperationException($"Could not find project at '{projectRoot}'");
}
var projectDestination = Path.Combine(destinationPath, project);
var projectDestinationDir = Directory.CreateDirectory(projectDestination);
CopyDirectory(new DirectoryInfo(projectRoot), projectDestinationDir);
SetupDirectoryBuildFiles(RepoRoot, testAppsRoot, projectDestination);
}
var directoryPath = Path.Combine(destinationPath, projectName);
var projectPath = Path.Combine(directoryPath, projectName + ".csproj");
CopyRepositoryAssets(destinationPath);
return new ProjectDirectory(
destinationPath,
directoryPath,
projectPath);
}
catch
{
CleanupDirectory(destinationPath);
throw;
}
static void CopyDirectory(DirectoryInfo source, DirectoryInfo destination, bool recursive = true)
{
foreach (var file in source.EnumerateFiles())
{
file.CopyTo(Path.Combine(destination.FullName, file.Name));
}
if (!recursive)
{
return;
}
foreach (var directory in source.EnumerateDirectories())
{
if (directory.Name == "bin")
{
// Just in case someone has opened the project in an IDE or built it. We don't want to copy
// these folders.
continue;
}
var created = destination.CreateSubdirectory(directory.Name);
if (directory.Name == "obj")
{
// Copy NuGet restore assets (viz all the files at the root of the obj directory, but stop there.)
CopyDirectory(directory, created, recursive: false);
}
else
{
CopyDirectory(directory, created);
}
}
}
static void SetupDirectoryBuildFiles(string repoRoot, string testAppsRoot, string projectDestination)
{
var beforeDirectoryPropsContent =
$@"<Project>
<PropertyGroup>
<RepoRoot>{repoRoot}</RepoRoot>
</PropertyGroup>
</Project>";
File.WriteAllText(Path.Combine(projectDestination, "Before.Directory.Build.props"), beforeDirectoryPropsContent);
new List<string> { "Directory.Build.props", "Directory.Build.targets", }
.ForEach(file =>
{
var source = Path.Combine(testAppsRoot, file);
var destination = Path.Combine(projectDestination, file);
File.Copy(source, destination);
});
}
static void CopyRepositoryAssets(string projectRoot)
{
const string GlobalJsonFileName = "global.json";
var globalJsonPath = Path.Combine(RepoRoot, GlobalJsonFileName);
var destinationFile = Path.Combine(projectRoot, GlobalJsonFileName);
File.Copy(globalJsonPath, destinationFile);
}
}
protected ProjectDirectory(string solutionPath, string directoryPath, string projectFilePath)
{
SolutionPath = solutionPath;
DirectoryPath = directoryPath;
ProjectFilePath = projectFilePath;
}
public string DirectoryPath { get; }
public string ProjectFilePath { get; }
public string SolutionPath { get; }
public string TargetFramework { get; set; } = "netstandard2.1";
#if DEBUG
public string Configuration => "Debug";
#elif RELEASE
public string Configuration => "Release";
#else
#error Configuration not supported
#endif
public string IntermediateOutputDirectory => Path.Combine("obj", Configuration, TargetFramework);
public string BuildOutputDirectory => Path.Combine("bin", Configuration, TargetFramework);
public string PublishOutputDirectory => Path.Combine(BuildOutputDirectory, "publish");
internal void AddProjectFileContent(string content)
{
if (content == null)
{
throw new ArgumentNullException(nameof(content));
}
var existing = File.ReadAllText(ProjectFilePath);
var updated = existing.Replace("<!-- Test Placeholder -->", content);
File.WriteAllText(ProjectFilePath, updated);
}
public void Dispose()
{
if (PreserveWorkingDirectory)
{
Console.WriteLine($"Skipping deletion of working directory {SolutionPath}");
}
else
{
CleanupDirectory(SolutionPath);
}
}
internal static void CleanupDirectory(string filePath)
{
var tries = 5;
var sleep = TimeSpan.FromSeconds(3);
for (var i = 0; i < tries; i++)
{
try
{
Directory.Delete(filePath, recursive: true);
return;
}
catch when (i < tries - 1)
{
Console.WriteLine($"Failed to delete directory {filePath}, trying again.");
Thread.Sleep(sleep);
}
}
}
private static string GetTestAttribute(string key)
{
return typeof(ProjectDirectory).Assembly
.GetCustomAttributes<AssemblyMetadataAttribute>()
.FirstOrDefault(f => f.Key == key)
?.Value;
}
}
}

View File

@ -0,0 +1,21 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Xunit;
namespace Microsoft.AspNetCore.Blazor.Build
{
public class ProjectDirectoryTest
{
[Fact]
public void ProjectDirectory_IsNotSetToBePreserved()
{
// Arrange
using var project = ProjectDirectory.Create("standalone");
// Act & Assert
// This flag is only meant for local debugging and should not be set when checking in.
Assert.False(project.PreserveWorkingDirectory);
}
}
}

View File

@ -0,0 +1,224 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.IO;
using System.Threading.Tasks;
using Xunit;
namespace Microsoft.AspNetCore.Blazor.Build
{
public class PublishIntegrationTest
{
[Fact]
public async Task Publish_WithDefaultSettings_Works()
{
// Arrange
using var project = ProjectDirectory.Create("standalone", additionalProjects: new [] { "razorclasslibrary" });
var result = await MSBuildProcessManager.DotnetMSBuild(project, "Publish");
Assert.BuildPassed(result);
var publishDirectory = project.PublishOutputDirectory;
var blazorPublishDirectory = Path.Combine(publishDirectory, Path.GetFileNameWithoutExtension(project.ProjectFilePath));
Assert.FileExists(result, blazorPublishDirectory, "dist", "_framework", "blazor.boot.json");
Assert.FileExists(result, blazorPublishDirectory, "dist", "_framework", "blazor.webassembly.js");
Assert.FileExists(result, blazorPublishDirectory, "dist", "_framework", "wasm", "dotnet.wasm");
Assert.FileExists(result, blazorPublishDirectory, "dist", "_framework", "wasm", "dotnet.js");
Assert.FileExists(result, blazorPublishDirectory, "dist", "_framework", "_bin", "standalone.dll");
Assert.FileExists(result, blazorPublishDirectory, "dist", "_framework", "_bin", "Microsoft.Extensions.Logging.Abstractions.dll"); // Verify dependencies are part of the output.
// Verify referenced static web assets
Assert.FileExists(result, blazorPublishDirectory, "dist", "_content", "RazorClassLibrary", "wwwroot", "exampleJsInterop.js");
Assert.FileExists(result, blazorPublishDirectory, "dist", "_content", "RazorClassLibrary", "styles.css");
// Verify static assets are in the publish directory
Assert.FileExists(result, blazorPublishDirectory, "dist", "index.html");
// Verify web.config
Assert.FileExists(result, publishDirectory, "web.config");
}
[Fact]
public async Task Publish_WithNoBuild_Works()
{
// Arrange
using var project = ProjectDirectory.Create("standalone", additionalProjects: new[] { "razorclasslibrary" });
var result = await MSBuildProcessManager.DotnetMSBuild(project, "Build");
Assert.BuildPassed(result);
result = await MSBuildProcessManager.DotnetMSBuild(project, "Publish", "/p:NoBuild=true");
Assert.BuildPassed(result);
var publishDirectory = project.PublishOutputDirectory;
var blazorPublishDirectory = Path.Combine(publishDirectory, Path.GetFileNameWithoutExtension(project.ProjectFilePath));
Assert.FileExists(result, blazorPublishDirectory, "dist", "_framework", "blazor.boot.json");
Assert.FileExists(result, blazorPublishDirectory, "dist", "_framework", "blazor.webassembly.js");
Assert.FileExists(result, blazorPublishDirectory, "dist", "_framework", "wasm", "dotnet.wasm");
Assert.FileExists(result, blazorPublishDirectory, "dist", "_framework", "wasm", "dotnet.js");
Assert.FileExists(result, blazorPublishDirectory, "dist", "_framework", "_bin", "standalone.dll");
Assert.FileExists(result, blazorPublishDirectory, "dist", "_framework", "_bin", "Microsoft.Extensions.Logging.Abstractions.dll"); // Verify dependencies are part of the output.
// Verify static assets are in the publish directory
Assert.FileExists(result, blazorPublishDirectory, "dist", "index.html");
// Verify static web assets from referenced projects are copied.
// Uncomment once https://github.com/aspnet/AspNetCore/issues/17426 is resolved.
// Assert.FileExists(result, blazorPublishDirectory, "dist", "_content", "RazorClassLibrary", "wwwroot", "exampleJsInterop.js");
// Assert.FileExists(result, blazorPublishDirectory, "dist", "_content", "RazorClassLibrary", "styles.css");
// Verify static assets are in the publish directory
Assert.FileExists(result, blazorPublishDirectory, "dist", "index.html");
// Verify web.config
Assert.FileExists(result, publishDirectory, "web.config");
}
[Fact]
public async Task Publish_WithLinkOnBuildDisabled_Works()
{
// Arrange
using var project = ProjectDirectory.Create("standalone", additionalProjects: new [] { "razorclasslibrary" });
project.AddProjectFileContent(
@"<PropertyGroup>
<BlazorLinkOnBuild>false</BlazorLinkOnBuild>
</PropertyGroup>");
var result = await MSBuildProcessManager.DotnetMSBuild(project, "Publish");
Assert.BuildPassed(result);
var publishDirectory = project.PublishOutputDirectory;
var blazorPublishDirectory = Path.Combine(publishDirectory, Path.GetFileNameWithoutExtension(project.ProjectFilePath));
Assert.FileExists(result, blazorPublishDirectory, "dist", "_framework", "blazor.boot.json");
Assert.FileExists(result, blazorPublishDirectory, "dist", "_framework", "blazor.webassembly.js");
Assert.FileExists(result, blazorPublishDirectory, "dist", "_framework", "wasm", "dotnet.wasm");
Assert.FileExists(result, blazorPublishDirectory, "dist", "_framework", "wasm", "dotnet.js");
Assert.FileExists(result, blazorPublishDirectory, "dist", "_framework", "_bin", "standalone.dll");
Assert.FileExists(result, blazorPublishDirectory, "dist", "_framework", "_bin", "Microsoft.Extensions.Logging.Abstractions.dll"); // Verify dependencies are part of the output.
// Verify static assets are in the publish directory
Assert.FileExists(result, blazorPublishDirectory, "dist", "index.html");
// Verify referenced static web assets
Assert.FileExists(result, blazorPublishDirectory, "dist", "_content", "RazorClassLibrary", "wwwroot", "exampleJsInterop.js");
Assert.FileExists(result, blazorPublishDirectory, "dist", "_content", "RazorClassLibrary", "styles.css");
// Verify web.config
Assert.FileExists(result, publishDirectory, "web.config");
}
[Fact]
public async Task Publish_SatelliteAssemblies_AreCopiedToBuildOutput()
{
// Arrange
using var project = ProjectDirectory.Create("standalone", additionalProjects: new[] { "razorclasslibrary", "classlibrarywithsatelliteassemblies" });
project.AddProjectFileContent(
@"
<PropertyGroup>
<DefineConstants>$(DefineConstants);REFERENCE_classlibrarywithsatelliteassemblies</DefineConstants>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include=""..\classlibrarywithsatelliteassemblies\classlibrarywithsatelliteassemblies.csproj"" />
</ItemGroup>");
var result = await MSBuildProcessManager.DotnetMSBuild(project, "Publish", args: "/restore");
Assert.BuildPassed(result);
var publishDirectory = project.PublishOutputDirectory;
var blazorPublishDirectory = Path.Combine(publishDirectory, Path.GetFileNameWithoutExtension(project.ProjectFilePath));
Assert.FileExists(result, blazorPublishDirectory, "dist", "_framework", "_bin", "Microsoft.CodeAnalysis.CSharp.dll");
Assert.FileExists(result, blazorPublishDirectory, "dist", "_framework", "_bin", "fr", "Microsoft.CodeAnalysis.CSharp.resources.dll"); // Verify satellite assemblies are present in the build output.
var bootJsonPath = Path.Combine(blazorPublishDirectory, "dist", "_framework", "blazor.boot.json");
Assert.FileContains(result, bootJsonPath, "\"Microsoft.CodeAnalysis.CSharp.dll\"");
Assert.FileContains(result, bootJsonPath, "\"fr\\/Microsoft.CodeAnalysis.CSharp.resources.dll\"");
}
[Fact]
public async Task Publish_HostedApp_Works()
{
// Arrange
using var project = ProjectDirectory.Create("blazorhosted", additionalProjects: new[] { "standalone", "razorclasslibrary", });
project.TargetFramework = "netcoreapp3.1";
var result = await MSBuildProcessManager.DotnetMSBuild(project, "Publish");
Assert.BuildPassed(result);
var publishDirectory = project.PublishOutputDirectory;
// Make sure the main project exists
Assert.FileExists(result, publishDirectory, "blazorhosted.dll");
var blazorPublishDirectory = Path.Combine(publishDirectory, "standalone");
Assert.FileExists(result, blazorPublishDirectory, "dist", "_framework", "blazor.boot.json");
Assert.FileExists(result, blazorPublishDirectory, "dist", "_framework", "blazor.webassembly.js");
Assert.FileExists(result, blazorPublishDirectory, "dist", "_framework", "wasm", "dotnet.wasm");
Assert.FileExists(result, blazorPublishDirectory, "dist", "_framework", "wasm", "dotnet.js");
Assert.FileExists(result, blazorPublishDirectory, "dist", "_framework", "_bin", "standalone.dll");
Assert.FileExists(result, blazorPublishDirectory, "dist", "_framework", "_bin", "Microsoft.Extensions.Logging.Abstractions.dll"); // Verify dependencies are part of the output.
// Verify static assets are in the publish directory
Assert.FileExists(result, blazorPublishDirectory, "dist", "index.html");
// Verify static web assets from referenced projects are copied.
Assert.FileExists(result, publishDirectory, "wwwroot", "_content", "RazorClassLibrary", "wwwroot", "exampleJsInterop.js");
Assert.FileExists(result, publishDirectory, "wwwroot", "_content", "RazorClassLibrary", "styles.css");
// Verify static assets are in the publish directory
Assert.FileExists(result, blazorPublishDirectory, "dist", "index.html");
// Verify web.config
Assert.FileExists(result, publishDirectory, "web.config");
var blazorConfig = Path.Combine(result.Project.DirectoryPath, publishDirectory, "standalone.blazor.config");
var blazorConfigLines = File.ReadAllLines(blazorConfig);
Assert.Equal(".", blazorConfigLines[0]);
Assert.Equal("standalone/", blazorConfigLines[1]);
}
[Fact]
public async Task Publish_HostedApp_WithNoBuild_Works()
{
// Arrange
using var project = ProjectDirectory.Create("blazorhosted", additionalProjects: new[] { "standalone", "razorclasslibrary", });
project.TargetFramework = "netcoreapp3.1";
var result = await MSBuildProcessManager.DotnetMSBuild(project, "Build");
Assert.BuildPassed(result);
result = await MSBuildProcessManager.DotnetMSBuild(project, "Publish", "/p:NoBuild=true");
var publishDirectory = project.PublishOutputDirectory;
// Make sure the main project exists
Assert.FileExists(result, publishDirectory, "blazorhosted.dll");
var blazorPublishDirectory = Path.Combine(publishDirectory, "standalone");
Assert.FileExists(result, blazorPublishDirectory, "dist", "_framework", "blazor.boot.json");
Assert.FileExists(result, blazorPublishDirectory, "dist", "_framework", "blazor.webassembly.js");
Assert.FileExists(result, blazorPublishDirectory, "dist", "_framework", "wasm", "dotnet.wasm");
Assert.FileExists(result, blazorPublishDirectory, "dist", "_framework", "wasm", "dotnet.js");
Assert.FileExists(result, blazorPublishDirectory, "dist", "_framework", "_bin", "standalone.dll");
Assert.FileExists(result, blazorPublishDirectory, "dist", "_framework", "_bin", "Microsoft.Extensions.Logging.Abstractions.dll"); // Verify dependencies are part of the output.
// Verify static assets are in the publish directory
Assert.FileExists(result, blazorPublishDirectory, "dist", "index.html");
// Verify static web assets from referenced projects are copied.
// Uncomment once https://github.com/aspnet/AspNetCore/issues/17426 is resolved.
// Assert.FileExists(result, publishDirectory, "wwwroot", "_content", "RazorClassLibrary", "wwwroot", "exampleJsInterop.js");
// Assert.FileExists(result, publishDirectory, "wwwroot", "_content", "RazorClassLibrary", "styles.css");
// Verify static assets are in the publish directory
Assert.FileExists(result, blazorPublishDirectory, "dist", "index.html");
// Verify web.config
Assert.FileExists(result, publishDirectory, "web.config");
}
}
}

View File

@ -9,7 +9,7 @@ using System.Text;
using Microsoft.AspNetCore.Testing;
using Xunit;
namespace Microsoft.AspNetCore.Blazor.Build.Test
namespace Microsoft.AspNetCore.Blazor.Build
{
public class RuntimeDependenciesResolverTest
{
@ -109,7 +109,7 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
// Act
var paths = RuntimeDependenciesResolver
var paths = ResolveBlazorRuntimeDependencies
.ResolveRuntimeDependenciesCore(
mainAssemblyLocation,
references,

View File

@ -0,0 +1,31 @@
<Project>
<Import Project="Before.Directory.Build.props" Condition="Exists('Before.Directory.Build.props')" />
<PropertyGroup>
<RepoRoot Condition="'$(RepoRoot)' ==''">$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), global.json))\</RepoRoot>
<ComponentsRoot>$(RepoRoot)src\Components\</ComponentsRoot>
<BlazorBuildRoot>$(ComponentsRoot)Blazor\Build\src\</BlazorBuildRoot>
<ReferenceBlazorBuildFromSourceProps>$(BlazorBuildRoot)ReferenceBlazorBuildFromSource.props</ReferenceBlazorBuildFromSourceProps>
<!-- Workaround for https://github.com/aspnet/AspNetCore/issues/17308 -->
<DefaultNetCoreTargetFramework>netcoreapp3.1</DefaultNetCoreTargetFramework>
<EnableSourceLink>false</EnableSourceLink>
<DeterministicSourcePaths>false</DeterministicSourcePaths>
</PropertyGroup>
<Import Project="$(RepoRoot)eng\Versions.props" />
<ItemGroup>
<!-- Use the sample compiler \ SDK that the rest of our build uses-->
<PackageReference Include="Microsoft.Net.Compilers.Toolset"
Version="$(MicrosoftNetCompilersToolsetPackageVersion)"
PrivateAssets="all"
IsImplicitlyDefined="true" />
<PackageReference Include="Microsoft.NET.Sdk.Razor"
Version="$(MicrosoftNETSdkRazorPackageVersion)"
PrivateAssets="All"
IsImplicitlyDefined="true" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,15 @@
using System;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
namespace blazorhosted.Server
{
public class Program
{
public static void Main(string[] args)
{
Console.WriteLine(typeof(IWebHost));
}
}
}

View File

@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<DisableImplicitComponentsAnalyzers>true</DisableImplicitComponentsAnalyzers>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\standalone\standalone.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,12 @@
using System;
namespace classlibrarywithsatelliteassemblies
{
public class Class1
{
public static void Test()
{
GC.KeepAlive(typeof(Microsoft.CodeAnalysis.CSharp.CSharpCompilation));
}
}
}

View File

@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<RazorLangVersion>3.0</RazorLangVersion>
</PropertyGroup>
<ItemGroup>
<!-- The compiler package contains quite a few satellite assemblies -->
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.4.0" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<RazorLangVersion>3.0</RazorLangVersion>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,8 @@
<Router AppAssembly="@typeof(Program).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData"/>
</Found>
<NotFound>
<p>Sorry, there's nothing at this address.</p>
</NotFound>
</Router>

View File

@ -0,0 +1,5 @@
@page "/"
<h1>Hello, world!</h1>
Welcome to your new app.

View File

@ -0,0 +1,14 @@
using System;
namespace standalone
{
public class Program
{
public static void Main(string[] args)
{
#if REFERENCE_classlibrarywithsatelliteassemblies
GC.KeepAlive(typeof(classlibrarywithsatelliteassemblies.Class1));
#endif
}
}
}

View File

@ -0,0 +1,2 @@
@using Microsoft.AspNetCore.Components.Routing
@using standalone

View File

@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Import Project="$(ReferenceBlazorBuildFromSourceProps)" />
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<RazorLangVersion>3.0</RazorLangVersion>
</PropertyGroup>
<!-- Test Placeholder -->
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components" Version="3.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Blazor.Mono" Version="$(MicrosoftAspNetCoreBlazorMonoPackageVersion)" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\razorclasslibrary\RazorClassLibrary.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,24 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>standalone</title>
<base href="/" />
<link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
<link href="css/site.css" rel="stylesheet" />
</head>
<body>
<app>Loading...</app>
<div id="blazor-error-ui">
An unhandled error has occurred.
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>
<script src="_framework/blazor.webassembly.js"></script>
</body>
</html>

View File

@ -11,15 +11,14 @@
<Description>Development server for use when building Blazor applications.</Description>
<!-- Set this to false because assemblies should not reference this assembly directly, (except for tests, of course). -->
<IsProjectReferenceProvider>false</IsProjectReferenceProvider>
<!-- This is so that we add the FrameworkReference to Microsoft.AspNetCore.App -->
<UseLatestAspNetCoreReference>true</UseLatestAspNetCoreReference>
</PropertyGroup>
<ItemGroup>
<Reference Include="Microsoft.AspNetCore.Blazor.Server" />
<Reference Include="Microsoft.AspNetCore.Components.Server" />
<Reference Include="Microsoft.AspNetCore.ResponseCompression" />
<Reference Include="Microsoft.AspNetCore" />
<Reference Include="Microsoft.Extensions.CommandLineUtils.Sources" />
<Reference Include="Microsoft.Extensions.Hosting" />
</ItemGroup>
<!-- Pack settings -->

View File

@ -4,4 +4,5 @@
<PropertyGroup>
<ComponentsPackageVersion>$(PackageVersion)</ComponentsPackageVersion>
</PropertyGroup>
</Project>

View File

@ -4,6 +4,7 @@
<TargetFramework>netstandard2.0</TargetFramework>
<Description>Provides experimental support for using System.Text.Json with HttpClient. Intended for use with Blazor running under WebAssembly.</Description>
<IsShippingPackage>false</IsShippingPackage>
<HasReferenceAssembly>false</HasReferenceAssembly>
</PropertyGroup>
<ItemGroup>

View File

@ -0,0 +1,25 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Runtime.CompilerServices;
namespace WebAssembly.JSInterop
{
/// <summary>
/// Methods that map to the functions compiled into the Mono WebAssembly runtime,
/// as defined by 'mono_add_internal_call' calls in driver.c
/// </summary>
internal class InternalCalls
{
// The exact namespace, type, and method names must match the corresponding entries
// in driver.c in the Mono distribution
// We're passing asyncHandle by ref not because we want it to be writable, but so it gets
// passed as a pointer (4 bytes). We can pass 4-byte values, but not 8-byte ones.
[MethodImpl(MethodImplOptions.InternalCall)]
public static extern string InvokeJSMarshalled(out string exception, ref long asyncHandle, string functionIdentifier, string argsJson);
[MethodImpl(MethodImplOptions.InternalCall)]
public static extern TRes InvokeJSUnmarshalled<T0, T1, T2, TRes>(out string exception, string functionIdentifier, T0 arg0, T1 arg1, T2 arg2);
}
}

View File

@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<Description>Abstractions and features for interop between Mono WebAssembly and JavaScript code.</Description>
<PackageTags>wasm;javascript;interop</PackageTags>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<IsPackable>true</IsPackable>
<IsShipping>true</IsShipping>
<HasReferenceAssembly>false</HasReferenceAssembly>
</PropertyGroup>
<ItemGroup>
<Reference Include="Microsoft.JSInterop" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,157 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Text.Json;
using Microsoft.JSInterop;
using Microsoft.JSInterop.Infrastructure;
using WebAssembly.JSInterop;
namespace Mono.WebAssembly.Interop
{
/// <summary>
/// Provides methods for invoking JavaScript functions for applications running
/// on the Mono WebAssembly runtime.
/// </summary>
public class MonoWebAssemblyJSRuntime : JSInProcessRuntime
{
/// <summary>
/// Gets the <see cref="MonoWebAssemblyJSRuntime"/> used to perform operations using <see cref="DotNetDispatcher"/>.
/// </summary>
private static MonoWebAssemblyJSRuntime Instance { get; set; }
/// <summary>
/// Initializes the <see cref="MonoWebAssemblyJSRuntime"/> to be used to perform operations using <see cref="DotNetDispatcher"/>.
/// </summary>
/// <param name="jsRuntime">The <see cref="MonoWebAssemblyJSRuntime"/> instance.</param>
protected static void Initialize(MonoWebAssemblyJSRuntime jsRuntime)
{
if (Instance != null)
{
throw new InvalidOperationException("MonoWebAssemblyJSRuntime has already been initialized.");
}
Instance = jsRuntime ?? throw new ArgumentNullException(nameof(jsRuntime));
}
/// <inheritdoc />
protected override string InvokeJS(string identifier, string argsJson)
{
var noAsyncHandle = default(long);
var result = InternalCalls.InvokeJSMarshalled(out var exception, ref noAsyncHandle, identifier, argsJson);
return exception != null
? throw new JSException(exception)
: result;
}
/// <inheritdoc />
protected override void BeginInvokeJS(long asyncHandle, string identifier, string argsJson)
{
InternalCalls.InvokeJSMarshalled(out _, ref asyncHandle, identifier, argsJson);
}
// Invoked via Mono's JS interop mechanism (invoke_method)
private static string InvokeDotNet(string assemblyName, string methodIdentifier, string dotNetObjectId, string argsJson)
{
var callInfo = new DotNetInvocationInfo(assemblyName, methodIdentifier, dotNetObjectId == null ? default : long.Parse(dotNetObjectId), callId: null);
return DotNetDispatcher.Invoke(Instance, callInfo, argsJson);
}
// Invoked via Mono's JS interop mechanism (invoke_method)
private static void EndInvokeJS(string argsJson)
=> DotNetDispatcher.EndInvokeJS(Instance, argsJson);
// Invoked via Mono's JS interop mechanism (invoke_method)
private static void BeginInvokeDotNet(string callId, string assemblyNameOrDotNetObjectId, string methodIdentifier, string argsJson)
{
// Figure out whether 'assemblyNameOrDotNetObjectId' is the assembly name or the instance ID
// We only need one for any given call. This helps to work around the limitation that we can
// only pass a maximum of 4 args in a call from JS to Mono WebAssembly.
string assemblyName;
long dotNetObjectId;
if (char.IsDigit(assemblyNameOrDotNetObjectId[0]))
{
dotNetObjectId = long.Parse(assemblyNameOrDotNetObjectId);
assemblyName = null;
}
else
{
dotNetObjectId = default;
assemblyName = assemblyNameOrDotNetObjectId;
}
var callInfo = new DotNetInvocationInfo(assemblyName, methodIdentifier, dotNetObjectId, callId);
DotNetDispatcher.BeginInvokeDotNet(Instance, callInfo, argsJson);
}
protected override void EndInvokeDotNet(DotNetInvocationInfo callInfo, in DotNetInvocationResult dispatchResult)
{
// For failures, the common case is to call EndInvokeDotNet with the Exception object.
// For these we'll serialize as something that's useful to receive on the JS side.
// If the value is not an Exception, we'll just rely on it being directly JSON-serializable.
var resultOrError = dispatchResult.Success ? dispatchResult.Result : dispatchResult.Exception.ToString();
// We pass 0 as the async handle because we don't want the JS-side code to
// send back any notification (we're just providing a result for an existing async call)
var args = JsonSerializer.Serialize(new[] { callInfo.CallId, dispatchResult.Success, resultOrError }, JsonSerializerOptions);
BeginInvokeJS(0, "DotNet.jsCallDispatcher.endInvokeDotNetFromJS", args);
}
#region Custom MonoWebAssemblyJSRuntime methods
/// <summary>
/// Invokes the JavaScript function registered with the specified identifier.
/// </summary>
/// <typeparam name="TRes">The .NET type corresponding to the function's return value type.</typeparam>
/// <param name="identifier">The identifier used when registering the target function.</param>
/// <returns>The result of the function invocation.</returns>
public TRes InvokeUnmarshalled<TRes>(string identifier)
=> InvokeUnmarshalled<object, object, object, TRes>(identifier, null, null, null);
/// <summary>
/// Invokes the JavaScript function registered with the specified identifier.
/// </summary>
/// <typeparam name="T0">The type of the first argument.</typeparam>
/// <typeparam name="TRes">The .NET type corresponding to the function's return value type.</typeparam>
/// <param name="identifier">The identifier used when registering the target function.</param>
/// <param name="arg0">The first argument.</param>
/// <returns>The result of the function invocation.</returns>
public TRes InvokeUnmarshalled<T0, TRes>(string identifier, T0 arg0)
=> InvokeUnmarshalled<T0, object, object, TRes>(identifier, arg0, null, null);
/// <summary>
/// Invokes the JavaScript function registered with the specified identifier.
/// </summary>
/// <typeparam name="T0">The type of the first argument.</typeparam>
/// <typeparam name="T1">The type of the second argument.</typeparam>
/// <typeparam name="TRes">The .NET type corresponding to the function's return value type.</typeparam>
/// <param name="identifier">The identifier used when registering the target function.</param>
/// <param name="arg0">The first argument.</param>
/// <param name="arg1">The second argument.</param>
/// <returns>The result of the function invocation.</returns>
public TRes InvokeUnmarshalled<T0, T1, TRes>(string identifier, T0 arg0, T1 arg1)
=> InvokeUnmarshalled<T0, T1, object, TRes>(identifier, arg0, arg1, null);
/// <summary>
/// Invokes the JavaScript function registered with the specified identifier.
/// </summary>
/// <typeparam name="T0">The type of the first argument.</typeparam>
/// <typeparam name="T1">The type of the second argument.</typeparam>
/// <typeparam name="T2">The type of the third argument.</typeparam>
/// <typeparam name="TRes">The .NET type corresponding to the function's return value type.</typeparam>
/// <param name="identifier">The identifier used when registering the target function.</param>
/// <param name="arg0">The first argument.</param>
/// <param name="arg1">The second argument.</param>
/// <param name="arg2">The third argument.</param>
/// <returns>The result of the function invocation.</returns>
public TRes InvokeUnmarshalled<T0, T1, T2, TRes>(string identifier, T0 arg0, T1 arg1, T2 arg2)
{
var result = InternalCalls.InvokeJSUnmarshalled<T0, T1, T2, TRes>(out var exception, identifier, arg0, arg1, arg2);
return exception != null
? throw new JSException(exception)
: result;
}
#endregion
}
}

View File

@ -4,6 +4,9 @@
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
<Description>Runtime server features for ASP.NET Core Blazor applications.</Description>
<IsShippingPackage>false</IsShippingPackage>
<HasReferenceAssembly>false</HasReferenceAssembly>
<!-- This is so that we add the FrameworkReference to Microsoft.AspNetCore.App -->
<UseLatestAspNetCoreReference>true</UseLatestAspNetCoreReference>
</PropertyGroup>
<ItemGroup>
@ -11,11 +14,6 @@
</ItemGroup>
<ItemGroup>
<Reference Include="Microsoft.AspNetCore.Hosting.Abstractions" />
<Reference Include="Microsoft.AspNetCore.SpaServices.Extensions" />
<Reference Include="Microsoft.AspNetCore.StaticFiles" />
<Reference Include="Microsoft.AspNetCore.WebSockets" />
<Reference Include="Microsoft.Extensions.FileProviders.Embedded" />
<Reference Include="Newtonsoft.Json" />
<!-- Used by ws-proxy sources only. Remove this once we're able to consume ws-proxy as a NuGet package. -->

View File

@ -9,9 +9,9 @@ using System.Linq;
using System.Net;
using System.Net.Http;
using System.Runtime.InteropServices;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Newtonsoft.Json;
using WsProxy;
namespace Microsoft.AspNetCore.Builder
@ -21,6 +21,15 @@ namespace Microsoft.AspNetCore.Builder
/// </summary>
public static class BlazorMonoDebugProxyAppBuilderExtensions
{
private static JsonSerializerOptions JsonOptions = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
PropertyNameCaseInsensitive = true,
IgnoreNullValues = true
};
private static string DefaultDebuggerHost = "http://localhost:9222";
/// <summary>
/// Adds middleware for needed for debugging Blazor applications
/// inside Chromium dev tools.
@ -29,6 +38,8 @@ namespace Microsoft.AspNetCore.Builder
{
app.UseWebSockets();
app.UseVisualStudioDebuggerConnectionRequestHandlers();
app.Use((context, next) =>
{
var requestPath = context.Request.Path;
@ -52,6 +63,85 @@ namespace Microsoft.AspNetCore.Builder
});
}
private static string GetDebuggerHost()
{
var envVar = Environment.GetEnvironmentVariable("ASPNETCORE_WEBASSEMBLYDEBUGHOST");
if (string.IsNullOrEmpty(envVar))
{
return DefaultDebuggerHost;
}
else
{
return envVar;
}
}
private static int GetDebuggerPort()
{
var host = GetDebuggerHost();
return new Uri(host).Port;
}
private static void UseVisualStudioDebuggerConnectionRequestHandlers(this IApplicationBuilder app)
{
// Unfortunately VS doesn't send any deliberately distinguishing information so we know it's
// not a regular browser or API client. The closest we can do is look for the *absence* of a
// User-Agent header. In the future, we should try to get VS to send a special header to indicate
// this is a debugger metadata request.
app.Use(async (context, next) =>
{
var request = context.Request;
var requestPath = request.Path;
if (requestPath.StartsWithSegments("/json")
&& !request.Headers.ContainsKey("User-Agent"))
{
if (requestPath.Equals("/json", StringComparison.OrdinalIgnoreCase) || requestPath.Equals("/json/list", StringComparison.OrdinalIgnoreCase))
{
var availableTabs = await GetOpenedBrowserTabs();
// Filter the list to only include tabs displaying the requested app,
// but only during the "choose application to debug" phase. We can't apply
// the same filter during the "connecting" phase (/json/list), nor do we need to.
if (requestPath.Equals("/json"))
{
availableTabs = availableTabs.Where(tab => tab.Url.StartsWith($"{request.Scheme}://{request.Host}{request.PathBase}/"));
}
var proxiedTabInfos = availableTabs.Select(tab =>
{
var underlyingV8Endpoint = tab.WebSocketDebuggerUrl;
var proxiedV8Endpoint = $"ws://{request.Host}{request.PathBase}/_framework/debug/ws-proxy?browser={WebUtility.UrlEncode(underlyingV8Endpoint)}";
return new
{
description = "",
devtoolsFrontendUrl = "",
id = tab.Id,
title = tab.Title,
type = tab.Type,
url = tab.Url,
webSocketDebuggerUrl = proxiedV8Endpoint
};
});
context.Response.ContentType = "application/json";
await context.Response.WriteAsync(JsonSerializer.Serialize(proxiedTabInfos));
}
else if (requestPath.Equals("/json/version", StringComparison.OrdinalIgnoreCase))
{
var browserVersionJson = await GetBrowserVersionInfoAsync();
context.Response.ContentType = "application/json";
await context.Response.WriteAsync(browserVersionJson);
}
}
else
{
await next();
}
});
}
private static async Task DebugWebSocketProxyRequest(HttpContext context)
{
if (!context.WebSockets.IsWebSocketRequest)
@ -81,13 +171,13 @@ namespace Microsoft.AspNetCore.Builder
// TODO: Allow overriding port (but not hostname, as we're connecting to the
// local browser, not to the webserver serving the app)
var debuggerHost = "http://localhost:9222";
var debuggerHost = GetDebuggerHost();
var debuggerTabsListUrl = $"{debuggerHost}/json";
IEnumerable<BrowserTab> availableTabs;
try
{
availableTabs = await GetOpenedBrowserTabs(debuggerHost);
availableTabs = await GetOpenedBrowserTabs();
}
catch (Exception ex)
{
@ -147,28 +237,30 @@ namespace Microsoft.AspNetCore.Builder
var underlyingV8Endpoint = tabToDebug.WebSocketDebuggerUrl;
var proxyEndpoint = $"{request.Host}{request.PathBase}/_framework/debug/ws-proxy?browser={WebUtility.UrlEncode(underlyingV8Endpoint)}";
var devToolsUrlAbsolute = new Uri(debuggerHost + tabToDebug.DevtoolsFrontendUrl);
var devToolsUrlWithProxy = $"{devToolsUrlAbsolute.Scheme}://{devToolsUrlAbsolute.Authority}{devToolsUrlAbsolute.AbsolutePath}?ws={proxyEndpoint}";
var wsParamName = request.IsHttps ? "wss" : "ws";
var devToolsUrlWithProxy = $"{devToolsUrlAbsolute.Scheme}://{devToolsUrlAbsolute.Authority}{devToolsUrlAbsolute.AbsolutePath}?{wsParamName}={proxyEndpoint}";
context.Response.Redirect(devToolsUrlWithProxy);
}
private static string GetLaunchChromeInstructions(string appRootUrl)
{
var profilePath = Path.Combine(Path.GetTempPath(), "blazor-edge-debug");
var profilePath = Path.Combine(Path.GetTempPath(), "blazor-chrome-debug");
var debuggerPort = GetDebuggerPort();
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return $@"<p>Press Win+R and enter the following:</p>
<p><strong><code>chrome --remote-debugging-port=9222 --user-data-dir=""{profilePath}"" {appRootUrl}</code></strong></p>";
<p><strong><code>chrome --remote-debugging-port={debuggerPort} --user-data-dir=""{profilePath}"" {appRootUrl}</code></strong></p>";
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
return $@"<p>In a terminal window execute the following:</p>
<p><strong><code>google-chrome --remote-debugging-port=9222 --user-data-dir={profilePath} {appRootUrl}</code></strong></p>";
<p><strong><code>google-chrome --remote-debugging-port={debuggerPort} --user-data-dir={profilePath} {appRootUrl}</code></strong></p>";
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
return $@"<p>Execute the following:</p>
<p><strong><code>open /Applications/Google\ Chrome.app --args --remote-debugging-port=9222 --user-data-dir={profilePath} {appRootUrl}</code></strong></p>";
<p><strong><code>open /Applications/Google\ Chrome.app --args --remote-debugging-port={debuggerPort} --user-data-dir={profilePath} {appRootUrl}</code></strong></p>";
}
else
{
@ -178,17 +270,18 @@ namespace Microsoft.AspNetCore.Builder
private static string GetLaunchEdgeInstructions(string appRootUrl)
{
var profilePath = Path.Combine(Path.GetTempPath(), "blazor-chrome-debug");
var profilePath = Path.Combine(Path.GetTempPath(), "blazor-edge-debug");
var debugggerPort = GetDebuggerPort();
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return $@"<p>Press Win+R and enter the following:</p>
<p><strong><code>msedge --remote-debugging-port=9222 --user-data-dir=""{profilePath}"" {appRootUrl}</code></strong></p>";
<p><strong><code>msedge --remote-debugging-port={debugggerPort} --user-data-dir=""{profilePath}"" --no-first-run {appRootUrl}</code></strong></p>";
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
return $@"<p>In a terminal window execute the following:</p>
<p><strong><code>open /Applications/Microsoft\ Edge\ Dev.app --args --remote-debugging-port=9222 --user-data-dir={profilePath} {appRootUrl}</code></strong></p>";
<p><strong><code>open /Applications/Microsoft\ Edge\ Dev.app --args --remote-debugging-port={debugggerPort} --user-data-dir={profilePath} {appRootUrl}</code></strong></p>";
}
else
{
@ -196,17 +289,24 @@ namespace Microsoft.AspNetCore.Builder
}
}
private static async Task<IEnumerable<BrowserTab>> GetOpenedBrowserTabs(string debuggerHost)
private static async Task<string> GetBrowserVersionInfoAsync()
{
using (var httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(5) })
{
var jsonResponse = await httpClient.GetStringAsync($"{debuggerHost}/json");
return JsonConvert.DeserializeObject<BrowserTab[]>(jsonResponse);
}
using var httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(5) };
var debuggerHost = GetDebuggerHost();
return await httpClient.GetStringAsync($"{debuggerHost}/json/version");
}
private static async Task<IEnumerable<BrowserTab>> GetOpenedBrowserTabs()
{
using var httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(5) };
var debuggerHost = GetDebuggerHost();
var jsonResponse = await httpClient.GetStringAsync($"{debuggerHost}/json");
return JsonSerializer.Deserialize<BrowserTab[]>(jsonResponse, JsonOptions);
}
class BrowserTab
{
public string Id { get; set; }
public string Type { get; set; }
public string Url { get; set; }
public string Title { get; set; }

View File

@ -545,7 +545,7 @@ namespace WsProxy {
return assemblies.FirstOrDefault (a => a.Name.Equals (name, StringComparison.InvariantCultureIgnoreCase));
}
/*
/*
V8 uses zero based indexing for both line and column.
PPDBs uses one based indexing for both line and column.
*/
@ -598,7 +598,7 @@ namespace WsProxy {
PPDBs uses one based indexing for both line and column.
*/
static bool Match (SequencePoint sp, int line, int column)
{
{
var bp = (line: line + 1, column: column + 1);
if (sp.StartLine > bp.line || sp.EndLine < bp.line)

View File

@ -223,7 +223,7 @@ namespace WsProxy {
Info ("RUNTIME READY, PARTY TIME");
await RuntimeReady (token);
await SendCommand ("Debugger.resume", new JObject (), token);
SendEvent ("Mono.runtimeReady", new JObject (), token);
SendEvent ("Mono.runtimeReady", new JObject (), token);
}
async Task OnBreakPointHit (JObject args, CancellationToken token)
@ -274,12 +274,18 @@ namespace WsProxy {
var frames = new List<Frame> ();
int frame_id = 0;
var the_mono_frames = res.Value? ["result"]? ["value"]? ["frames"]?.Values<JObject> ();
foreach (var mono_frame in the_mono_frames) {
var il_pos = mono_frame ["il_pos"].Value<int> ();
var method_token = mono_frame ["method_token"].Value<int> ();
var assembly_name = mono_frame ["assembly_name"].Value<string> ();
var asm = store.GetAssemblyByName (assembly_name);
if (asm == null) {
Info ($"Unable to find assembly: {assembly_name}");
continue;
}
var method = asm.GetMethodByToken (method_token);
if (method == null) {
@ -374,7 +380,7 @@ namespace WsProxy {
//Debug ($"\t{is_ready}");
if (is_ready.HasValue && is_ready.Value == true) {
Debug ("RUNTIME LOOK READY. GO TIME!");
await RuntimeReady (token);
await OnRuntimeReady (token);
}
}
@ -426,33 +432,38 @@ namespace WsProxy {
return;
}
var values = res.Value?["result"]?["value"]?.Values<JObject>().ToArray();
try {
var values = res.Value?["result"]?["value"]?.Values<JObject>().ToArray() ?? Array.Empty<JObject>();
var var_list = new List<JObject>();
var var_list = new List<JObject>();
// Trying to inspect the stack frame for DotNetDispatcher::InvokeSynchronously
// results in a "Memory access out of bounds", causing 'values' to be null,
// so skip returning variable values in that case.
for (int i = 0; i < values.Length; i+=2)
{
string fieldName = (string)values[i]["name"];
if (fieldName.Contains("k__BackingField")){
fieldName = fieldName.Replace("k__BackingField", "");
fieldName = fieldName.Replace("<", "");
fieldName = fieldName.Replace(">", "");
}
var value = values [i + 1]? ["value"];
if (((string)value ["description"]) == null)
value ["description"] = value ["value"]?.ToString ();
// Trying to inspect the stack frame for DotNetDispatcher::InvokeSynchronously
// results in a "Memory access out of bounds", causing 'values' to be null,
// so skip returning variable values in that case.
for (int i = 0; i < values.Length; i+=2)
{
string fieldName = (string)values[i]["name"];
if (fieldName.Contains("k__BackingField")){
fieldName = fieldName.Replace("k__BackingField", "");
fieldName = fieldName.Replace("<", "");
fieldName = fieldName.Replace(">", "");
var_list.Add(JObject.FromObject(new {
name = fieldName,
value
}));
}
o = JObject.FromObject(new
{
result = var_list
});
} catch (Exception) {
Debug ($"failed to parse {res.Value}");
}
var_list.Add(JObject.FromObject(new
{
name = fieldName,
value = values[i+1]["value"]
}));
}
o = JObject.FromObject(new
{
result = var_list
});
SendResponse(msg_id, Result.Ok(o), token);
}
@ -481,41 +492,51 @@ namespace WsProxy {
return;
}
var values = res.Value? ["result"]? ["value"]?.Values<JObject> ().ToArray ();
try {
var values = res.Value? ["result"]? ["value"]?.Values<JObject> ().ToArray ();
var var_list = new List<JObject> ();
int i = 0;
// Trying to inspect the stack frame for DotNetDispatcher::InvokeSynchronously
// results in a "Memory access out of bounds", causing 'values' to be null,
// so skip returning variable values in that case.
while (values != null && i < vars.Length && i < values.Length) {
var value = values [i] ["value"];
if (((string)value ["description"]) == null)
value ["description"] = value ["value"]?.ToString();
var var_list = new List<JObject> ();
int i = 0;
// Trying to inspect the stack frame for DotNetDispatcher::InvokeSynchronously
// results in a "Memory access out of bounds", causing 'values' to be null,
// so skip returning variable values in that case.
while (values != null && i < vars.Length && i < values.Length) {
var value = values [i] ["value"];
if (((string)value ["description"]) == null)
value ["description"] = value ["value"]?.ToString ();
var_list.Add (JObject.FromObject (new {
name = vars [i].Name,
value = values [i] ["value"]
}));
i++;
var_list.Add (JObject.FromObject (new {
name = vars [i].Name,
value
}));
i++;
}
//Async methods are special in the way that local variables can be lifted to generated class fields
//value of "this" comes here either
while (i < values.Length) {
String name = values [i] ["name"].ToString ();
if (name.IndexOf (">", StringComparison.Ordinal) > 0)
name = name.Substring (1, name.IndexOf (">", StringComparison.Ordinal) - 1);
var value = values [i + 1] ["value"];
if (((string)value ["description"]) == null)
value ["description"] = value ["value"]?.ToString ();
var_list.Add (JObject.FromObject (new {
name,
value
}));
i = i + 2;
}
o = JObject.FromObject (new {
result = var_list
});
SendResponse (msg_id, Result.Ok (o), token);
}
//Async methods are special in the way that local variables can be lifted to generated class fields
//value of "this" comes here either
while (i < values.Length) {
String name = values [i] ["name"].ToString ();
if (name.IndexOf (">", StringComparison.Ordinal) > 0)
name = name.Substring (1, name.IndexOf (">", StringComparison.Ordinal) - 1);
var_list.Add (JObject.FromObject (new {
name = name,
value = values [i+1] ["value"]
}));
i = i + 2;
catch (Exception) {
SendResponse (msg_id, res, token);
}
o = JObject.FromObject (new {
result = var_list
});
SendResponse (msg_id, Result.Ok (o), token);
}
async Task<Result> EnableBreakPoint (Breakpoint bp, CancellationToken token)

View File

@ -97,6 +97,7 @@ namespace WsProxy {
internal class WsProxy {
TaskCompletionSource<bool> side_exception = new TaskCompletionSource<bool> ();
TaskCompletionSource<bool> client_initiated_close = new TaskCompletionSource<bool> ();
List<(int, TaskCompletionSource<Result>)> pending_cmds = new List<(int, TaskCompletionSource<Result>)> ();
ClientWebSocket browser;
WebSocket ide;
@ -119,8 +120,16 @@ namespace WsProxy {
byte [] buff = new byte [4000];
var mem = new MemoryStream ();
while (true) {
if (socket.State != WebSocketState.Open) {
Console.WriteLine ($"WSProxy: Socket is no longer open.");
client_initiated_close.TrySetResult (true);
return null;
}
var result = await socket.ReceiveAsync (new ArraySegment<byte> (buff), token);
if (result.MessageType == WebSocketMessageType.Close) {
client_initiated_close.TrySetResult (true);
return null;
}
@ -144,7 +153,7 @@ namespace WsProxy {
void Send (WebSocket to, JObject o, CancellationToken token)
{
var bytes = Encoding.UTF8.GetBytes (o.ToString ());
var bytes = Encoding.UTF8.GetBytes (o.ToString ());
var queue = GetQueueForSocket (to);
var task = queue.Send (bytes, token);
@ -199,9 +208,10 @@ namespace WsProxy {
void ProcessIdeMessage (string msg, CancellationToken token)
{
var res = JObject.Parse (msg);
pending_ops.Add (OnCommand (res ["id"].Value<int> (), res ["method"].Value<string> (), res ["params"] as JObject, token));
if (!string.IsNullOrEmpty (msg)) {
var res = JObject.Parse (msg);
pending_ops.Add (OnCommand (res ["id"].Value<int> (), res ["method"].Value<string> (), res ["params"] as JObject, token));
}
}
internal async Task<Result> SendCommand (string method, JObject args, CancellationToken token) {
@ -255,24 +265,25 @@ namespace WsProxy {
Send (this.ide, o, token);
}
// , HttpContext context)
// , HttpContext context)
public async Task Run (Uri browserUri, WebSocket ideSocket)
{
Debug ("wsproxy start");
Debug ($"WsProxy Starting on {browserUri}");
using (this.ide = ideSocket) {
Debug ("ide connected");
Debug ($"WsProxy: IDE waiting for connection on {browserUri}");
queues.Add (new WsQueue (this.ide));
using (this.browser = new ClientWebSocket ()) {
this.browser.Options.KeepAliveInterval = Timeout.InfiniteTimeSpan;
await this.browser.ConnectAsync (browserUri, CancellationToken.None);
queues.Add (new WsQueue (this.browser));
Debug ("client connected");
Debug ($"WsProxy: Client connected on {browserUri}");
var x = new CancellationTokenSource ();
pending_ops.Add (ReadOne (browser, x.Token));
pending_ops.Add (ReadOne (ide, x.Token));
pending_ops.Add (side_exception.Task);
pending_ops.Add (client_initiated_close.Task);
try {
while (!x.IsCancellationRequested) {
@ -280,15 +291,23 @@ namespace WsProxy {
//Console.WriteLine ("pump {0} {1}", task, pending_ops.IndexOf (task));
if (task == pending_ops [0]) {
var msg = ((Task<string>)task).Result;
pending_ops [0] = ReadOne (browser, x.Token); //queue next read
ProcessBrowserMessage (msg, x.Token);
if (msg != null) {
pending_ops [0] = ReadOne (browser, x.Token); //queue next read
ProcessBrowserMessage (msg, x.Token);
}
} else if (task == pending_ops [1]) {
var msg = ((Task<string>)task).Result;
pending_ops [1] = ReadOne (ide, x.Token); //queue next read
ProcessIdeMessage (msg, x.Token);
if (msg != null) {
pending_ops [1] = ReadOne (ide, x.Token); //queue next read
ProcessIdeMessage (msg, x.Token);
}
} else if (task == pending_ops [2]) {
var res = ((Task<bool>)task).Result;
throw new Exception ("side task must always complete with an exception, what's going on???");
} else if (task == pending_ops [3]) {
var res = ((Task<bool>)task).Result;
Debug ($"WsProxy: Client initiated close from {browserUri}");
x.Cancel ();
} else {
//must be a background task
pending_ops.Remove (task);
@ -301,10 +320,11 @@ namespace WsProxy {
}
}
} catch (Exception e) {
Debug ($"got exception {e}");
Debug ($"WsProxy::Run: Exception {e}");
//throw;
} finally {
x.Cancel ();
if (!x.IsCancellationRequested)
x.Cancel ();
}
}
}

View File

@ -1,15 +0,0 @@
<Project>
<Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory)..\, Directory.Build.props))\Directory.Build.props" />
<PropertyGroup>
<GenerateDocumentationFile>false</GenerateDocumentationFile>
<IncludeSymbols>false</IncludeSymbols>
</PropertyGroup>
<PropertyGroup Label="Package Versions">
<!-- Used only in development when running the template contents directly from source -->
<TemplateBlazorPackageVersion>0.8.0-preview-19064-0339</TemplateBlazorPackageVersion>
<TemplateComponentsPackageVersion>3.0.0-preview-19064-0339</TemplateComponentsPackageVersion>
</PropertyGroup>
</Project>

View File

@ -1,17 +0,0 @@
<Project>
<Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory)..\, Directory.Build.targets))\Directory.Build.targets" />
<Target Name="SetTemplateJsonSymbolReplacements">
<PropertyGroup>
<!--
Properties here will be injected into the template config *.json files
during the build, replacing tokens of the form ${PropertyName}
-->
<GeneratedContentProperties>
TemplateBlazorVersion=$(PackageVersion);
TemplateComponentsVersion=$(ComponentsPackageVersion);
RepositoryCommit=$(SourceRevisionId);
</GeneratedContentProperties>
</PropertyGroup>
</Target>
</Project>

View File

@ -1,16 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata>
$CommonMetadataElements$
<packageTypes>
<packageType name="Template" />
</packageTypes>
</metadata>
<files>
$CommonFileElements$
<file
src="content/**"
exclude="**/bin/**;**/obj/**;**/.template.config.src/**;content/Directory.Build.props;content/Directory.Build.targets;"
target="Content" />
</files>
</package>

View File

@ -1,20 +0,0 @@
using Microsoft.AspNetCore.Blazor.Hosting;
#if (Hosted)
namespace BlazorWasm_CSharp.Client
#else
namespace BlazorWasm_CSharp
#endif
{
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IWebAssemblyHostBuilder CreateHostBuilder(string[] args) =>
BlazorWebAssemblyHost.CreateDefaultBuilder()
.UseBlazorStartup<Startup>();
}
}

Some files were not shown because too many files have changed in this diff Show More