diff --git a/.azure/pipelines/blazor-daily-tests.yml b/.azure/pipelines/blazor-daily-tests.yml new file mode 100644 index 0000000000..537751bfed --- /dev/null +++ b/.azure/pipelines/blazor-daily-tests.yml @@ -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 \ No newline at end of file diff --git a/.azure/pipelines/ci.yml b/.azure/pipelines/ci.yml index 87d067bb56..db4461e991 100644 --- a/.azure/pipelines/ci.yml +++ b/.azure/pipelines/ci.yml @@ -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: diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json new file mode 100644 index 0000000000..be95a01fc5 --- /dev/null +++ b/.config/dotnet-tools.json @@ -0,0 +1,12 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "dotnet-serve": { + "version": "1.5.0", + "commands": [ + "dotnet-serve" + ] + } + } +} \ No newline at end of file diff --git a/NuGet.config b/NuGet.config index 389b3f7ed6..b632b50bdf 100644 --- a/NuGet.config +++ b/NuGet.config @@ -3,6 +3,7 @@ +<<<<<<< HEAD @@ -10,5 +11,24 @@ +======= + + + + + + + + + + + + + + + + + +>>>>>>> bbafecc0535e1de3264845e51ea8b3d18eb3ca61 diff --git a/eng/Build.props b/eng/Build.props index 42a4992e7d..6ba4d0eda6 100644 --- a/eng/Build.props +++ b/eng/Build.props @@ -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 @@ " /> + + + $(RepoRoot)src\Components\**\*.csproj;$(RepoRoot)src\ProjectTemplates\**\*.csproj + + diff --git a/eng/common/tools.ps1 b/eng/common/tools.ps1 index 92a053bd16..29d76cd3be 100644 --- a/eng/common/tools.ps1 +++ b/eng/common/tools.ps1 @@ -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 diff --git a/eng/scripts/CodeCheck.ps1 b/eng/scripts/CodeCheck.ps1 index 072f55fe21..a7baf079c8 100644 --- a/eng/scripts/CodeCheck.ps1 +++ b/eng/scripts/CodeCheck.ps1 @@ -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" diff --git a/global.json b/global.json index 974708a611..e0882dd64d 100644 --- a/global.json +++ b/global.json @@ -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 } } diff --git a/src/Components/Blazor/Blazor.Version.props b/src/Components/Blazor/Blazor.Version.props new file mode 100644 index 0000000000..123a94c1d7 --- /dev/null +++ b/src/Components/Blazor/Blazor.Version.props @@ -0,0 +1,8 @@ + + + + 3.2.0 + preview1 + + + \ No newline at end of file diff --git a/src/Components/Blazor/Blazor/ref/Microsoft.AspNetCore.Blazor.netstandard2.1.cs b/src/Components/Blazor/Blazor/ref/Microsoft.AspNetCore.Blazor.netstandard2.1.cs new file mode 100644 index 0000000000..eb33362975 --- /dev/null +++ b/src/Components/Blazor/Blazor/ref/Microsoft.AspNetCore.Blazor.netstandard2.1.cs @@ -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 + { + public RootComponentMappingCollection() { } + public void Add(System.Type componentType, string selector) { } + public void AddRange(System.Collections.Generic.IEnumerable items) { } + public void Add(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; } + } +} diff --git a/src/Components/Blazor/Blazor/src/Builder/ComponentsApplicationBuilderExtensions.cs b/src/Components/Blazor/Blazor/src/Builder/ComponentsApplicationBuilderExtensions.cs deleted file mode 100644 index 66532474c6..0000000000 --- a/src/Components/Blazor/Blazor/src/Builder/ComponentsApplicationBuilderExtensions.cs +++ /dev/null @@ -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 -{ - /// - /// Provides extension methods for . - /// - public static class ComponentsApplicationBuilderExtensions - { - /// - /// Associates the component type with the application, - /// causing it to be displayed in the specified DOM element. - /// - /// The . - /// The type of the component. - /// A CSS selector that uniquely identifies a DOM element. - public static void AddComponent(this IComponentsApplicationBuilder app, string domElementSelector) - where TComponent : IComponent - { - app.AddComponent(typeof(TComponent), domElementSelector); - } - } -} diff --git a/src/Components/Blazor/Blazor/src/Builder/IComponentsApplicationBuilder.cs b/src/Components/Blazor/Blazor/src/Builder/IComponentsApplicationBuilder.cs deleted file mode 100644 index 84806a8769..0000000000 --- a/src/Components/Blazor/Blazor/src/Builder/IComponentsApplicationBuilder.cs +++ /dev/null @@ -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 -{ - /// - /// A builder for adding components to an application. - /// - public interface IComponentsApplicationBuilder - { - /// - /// Gets the application services. - /// - IServiceProvider Services { get; } - - /// - /// Associates the with the application, - /// causing it to be displayed in the specified DOM element. - /// - /// The type of the component. - /// A CSS selector that uniquely identifies a DOM element. - void AddComponent(Type componentType, string domElementSelector); - } -} diff --git a/src/Components/Blazor/Blazor/src/Hosting/BlazorWebAssemblyHost.cs b/src/Components/Blazor/Blazor/src/Hosting/BlazorWebAssemblyHost.cs deleted file mode 100644 index e5631805ef..0000000000 --- a/src/Components/Blazor/Blazor/src/Hosting/BlazorWebAssemblyHost.cs +++ /dev/null @@ -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 -{ - /// - /// Used to create an instance of Blazor host builder for a Browser application. - /// - public static class BlazorWebAssemblyHost - { - /// - /// Creates an instance of . - /// - /// The . - public static IWebAssemblyHostBuilder CreateDefaultBuilder() - { - return new WebAssemblyHostBuilder(); - } - } -} diff --git a/src/Components/Blazor/Blazor/src/Hosting/ConventionBasedStartup.cs b/src/Components/Blazor/Blazor/src/Hosting/ConventionBasedStartup.cs deleted file mode 100644 index 53eecbf6e0..0000000000 --- a/src/Components/Blazor/Blazor/src/Hosting/ConventionBasedStartup.cs +++ /dev/null @@ -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()); - } - } -} \ No newline at end of file diff --git a/src/Components/Blazor/Blazor/src/Hosting/EntrypointInvoker.cs b/src/Components/Blazor/Blazor/src/Hosting/EntrypointInvoker.cs new file mode 100644 index 0000000000..40a5d07de4 --- /dev/null +++ b/src/Components/Blazor/Blazor/src/Hosting/EntrypointInvoker.cs @@ -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() } : 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 "
" + // that is marked as the assembly entrypoint. Detect this case, and instead of + // calling "", 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); + } + } +} diff --git a/src/Components/Blazor/Blazor/src/Hosting/IBlazorStartup.cs b/src/Components/Blazor/Blazor/src/Hosting/IBlazorStartup.cs deleted file mode 100644 index e87138387f..0000000000 --- a/src/Components/Blazor/Blazor/src/Hosting/IBlazorStartup.cs +++ /dev/null @@ -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); - } -} diff --git a/src/Components/Blazor/Blazor/src/Hosting/IWebAssemblyHost.cs b/src/Components/Blazor/Blazor/src/Hosting/IWebAssemblyHost.cs deleted file mode 100644 index 8f21f93421..0000000000 --- a/src/Components/Blazor/Blazor/src/Hosting/IWebAssemblyHost.cs +++ /dev/null @@ -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 -{ - /// - /// A program abstraction. - /// - public interface IWebAssemblyHost : IDisposable - { - /// - /// The programs configured services. - /// - IServiceProvider Services { get; } - - /// - /// Start the program. - /// - /// Used to abort program start. - /// - Task StartAsync(CancellationToken cancellationToken = default); - - /// - /// Attempts to gracefully stop the program. - /// - /// Used to indicate when stop should no longer be graceful. - /// - Task StopAsync(CancellationToken cancellationToken = default); - } -} diff --git a/src/Components/Blazor/Blazor/src/Hosting/IWebAssemblyHostBuilder.cs b/src/Components/Blazor/Blazor/src/Hosting/IWebAssemblyHostBuilder.cs deleted file mode 100644 index c5a52bc280..0000000000 --- a/src/Components/Blazor/Blazor/src/Hosting/IWebAssemblyHostBuilder.cs +++ /dev/null @@ -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 -{ - /// - /// Abstraction for configuring a Blazor browser-based application. - /// - public interface IWebAssemblyHostBuilder - { - /// - /// A central location for sharing state between components during the host building process. - /// - IDictionary Properties { get; } - - /// - /// Overrides the factory used to create the service provider. - /// - /// The same instance of the for chaining. - IWebAssemblyHostBuilder UseServiceProviderFactory(IServiceProviderFactory factory); - - /// - /// Overrides the factory used to create the service provider. - /// - /// The same instance of the for chaining. - IWebAssemblyHostBuilder UseServiceProviderFactory(Func> factory); - - /// - /// Adds services to the container. This can be called multiple times and the results will be additive. - /// - /// The delegate for configuring the that will be used - /// to construct the . - /// The same instance of the for chaining. - IWebAssemblyHostBuilder ConfigureServices(Action configureDelegate); - - /// - /// Run the given actions to initialize the host. This can only be called once. - /// - /// An initialized - IWebAssemblyHost Build(); - } -} diff --git a/src/Components/Blazor/Blazor/src/Hosting/IWebAssemblyServiceFactoryAdapter.cs b/src/Components/Blazor/Blazor/src/Hosting/IWebAssemblyServiceFactoryAdapter.cs deleted file mode 100644 index c790a3c879..0000000000 --- a/src/Components/Blazor/Blazor/src/Hosting/IWebAssemblyServiceFactoryAdapter.cs +++ /dev/null @@ -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); - } -} diff --git a/src/Components/Blazor/Blazor/src/Hosting/RootComponentMapping.cs b/src/Components/Blazor/Blazor/src/Hosting/RootComponentMapping.cs new file mode 100644 index 0000000000..53b34d8822 --- /dev/null +++ b/src/Components/Blazor/Blazor/src/Hosting/RootComponentMapping.cs @@ -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 +{ + /// + /// Defines a mapping between a root and a DOM element selector. + /// + public readonly struct RootComponentMapping + { + /// + /// Creates a new instance of with the provided + /// and . + /// + /// The component type. Must implement . + /// The DOM element selector. + 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; + } + + /// + /// Gets the component type. + /// + public Type ComponentType { get; } + + /// + /// Gets the DOM element selector. + /// + public string Selector { get; } + } +} diff --git a/src/Components/Blazor/Blazor/src/Hosting/RootComponentMappingCollection.cs b/src/Components/Blazor/Blazor/src/Hosting/RootComponentMappingCollection.cs new file mode 100644 index 0000000000..0e488f0deb --- /dev/null +++ b/src/Components/Blazor/Blazor/src/Hosting/RootComponentMappingCollection.cs @@ -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 +{ + /// + /// Defines a collection of items. + /// + public class RootComponentMappingCollection : Collection + { + /// + /// Adds a component mapping to the collection. + /// + /// The component type. + /// The DOM element selector. + public void Add(string selector) where TComponent : IComponent + { + if (selector is null) + { + throw new ArgumentNullException(nameof(selector)); + } + + Add(new RootComponentMapping(typeof(TComponent), selector)); + } + + /// + /// Adds a component mapping to the collection. + /// + /// The component type. Must implement . + /// The DOM element selector. + 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)); + } + + /// + /// Adds a collection of items to this collection. + /// + /// The items to add. + public void AddRange(IEnumerable items) + { + if (items is null) + { + throw new ArgumentNullException(nameof(items)); + } + + foreach (var item in items) + { + Add(item); + } + } + } +} diff --git a/src/Components/Blazor/Blazor/src/Hosting/WebAssemblyBlazorApplicationBuilder.cs b/src/Components/Blazor/Blazor/src/Hosting/WebAssemblyBlazorApplicationBuilder.cs deleted file mode 100644 index 8cb5d87561..0000000000 --- a/src/Components/Blazor/Blazor/src/Hosting/WebAssemblyBlazorApplicationBuilder.cs +++ /dev/null @@ -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 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; - } - } -} diff --git a/src/Components/Blazor/Blazor/src/Hosting/WebAssemblyHost.cs b/src/Components/Blazor/Blazor/src/Hosting/WebAssemblyHost.cs index 3a2ccfbaae..02c2693ff7 100644 --- a/src/Components/Blazor/Blazor/src/Hosting/WebAssemblyHost.cs +++ b/src/Components/Blazor/Blazor/src/Hosting/WebAssemblyHost.cs @@ -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 + /// + /// A host object for Blazor running under WebAssembly. Use + /// to initialize a . + /// + 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; } + /// + /// Gets the application configuration. + /// + public IConfiguration Configuration => _configuration; - public Task StartAsync(CancellationToken cancellationToken = default) + /// + /// Gets the service provider associated with the application. + /// + public IServiceProvider Services => _scope.ServiceProvider; + + /// + /// Disposes the host asynchronously. + /// + /// A which respresents the completion of disposal. + public async ValueTask DisposeAsync() { - return StartAsyncAwaited(); - } - - private async Task StartAsyncAwaited() - { - var scopeFactory = Services.GetRequiredService(); - _scope = scopeFactory.CreateScope(); - - try + if (_disposed) { - var startup = _scope.ServiceProvider.GetService(); - 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(); + } + } + + /// + /// Runs the application associated with this host. + /// + /// A which represents exit of the application. + /// + /// 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. + /// + 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(); + + using (cancellationToken.Register(() => { tcs.TrySetResult(null); })) + { + var loggerFactory = Services.GetRequiredService(); + _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(); } } } diff --git a/src/Components/Blazor/Blazor/src/Hosting/WebAssemblyHostBuilder.cs b/src/Components/Blazor/Blazor/src/Hosting/WebAssemblyHostBuilder.cs index 7fa4dd0ae1..1d1f05dce7 100644 --- a/src/Components/Blazor/Blazor/src/Hosting/WebAssemblyHostBuilder.cs +++ b/src/Components/Blazor/Blazor/src/Hosting/WebAssemblyHostBuilder.cs @@ -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 + /// + /// A builder for configuring and creating a . + /// + public sealed class WebAssemblyHostBuilder { - private List> _configureServicesActions = new List>(); - private bool _hostBuilt; - private WebAssemblyHostBuilderContext _BrowserHostBuilderContext; - private IWebAssemblyServiceFactoryAdapter _serviceProviderFactory = new WebAssemblyServiceFactoryAdapter(new DefaultServiceProviderFactory()); - private IServiceProvider _appServices; - /// - /// A central location for sharing state between components during the host building process. + /// Creates an instance of using the most common + /// conventions and settings. /// - public IDictionary Properties { get; } = new Dictionary(); - - /// - /// Adds services to the container. This can be called multiple times and the results will be additive. - /// - /// - /// The same instance of the for chaining. - public IWebAssemblyHostBuilder ConfigureServices(Action configureDelegate) + /// The argument passed to the application's main method. + /// A . + 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(); + 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; } /// - /// Overrides the factory used to create the service provider. + /// Creates an instance of with the minimal configuration. /// - /// The same instance of the for chaining. - public IWebAssemblyHostBuilder UseServiceProviderFactory(IServiceProviderFactory factory) + private WebAssemblyHostBuilder() { - _serviceProviderFactory = new WebAssemblyServiceFactoryAdapter(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(); } /// - /// Overrides the factory used to create the service provider. + /// Gets an that can be used to customize the application's + /// configuration sources. /// - /// The same instance of the for chaining. - public IWebAssemblyHostBuilder UseServiceProviderFactory(Func> factory) - { - _serviceProviderFactory = new WebAssemblyServiceFactoryAdapter(() => _BrowserHostBuilderContext, factory ?? throw new ArgumentNullException(nameof(factory))); - return this; - } + public IConfigurationBuilder Configuration { get; } /// - /// 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. /// - /// An initialized - public IWebAssemblyHost Build() + public RootComponentMappingCollection RootComponents { get; } + + /// + /// Gets the service collection. + /// + public IServiceCollection Services { get; } + + /// + /// Builds a instance based on the configuration of this builder. + /// + /// A object. + 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(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().CreateScope(); - return _appServices.GetRequiredService(); + 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(); - services.AddSingleton(WebAssemblyJSRuntime.Instance); - services.AddSingleton(WebAssemblyNavigationManager.Instance); - services.AddSingleton(WebAssemblyNavigationInterception.Instance); - services.AddSingleton(); - services.AddSingleton(s => + Services.AddSingleton(WebAssemblyJSRuntime.Instance); + Services.AddSingleton(WebAssemblyNavigationManager.Instance); + Services.AddSingleton(WebAssemblyNavigationInterception.Instance); + Services.AddSingleton(); + Services.TryAdd(ServiceDescriptor.Singleton(typeof(ILogger<>), typeof(WebAssemblyConsoleLogger<>))); + Services.AddSingleton(s => { // Creating the URI helper needs to wait until the JS Runtime is initialized, so defer it. var navigationManager = s.GetRequiredService(); @@ -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); } } } diff --git a/src/Components/Blazor/Blazor/src/Hosting/WebAssemblyHostBuilderContext.cs b/src/Components/Blazor/Blazor/src/Hosting/WebAssemblyHostBuilderContext.cs deleted file mode 100644 index c7b7dd6f19..0000000000 --- a/src/Components/Blazor/Blazor/src/Hosting/WebAssemblyHostBuilderContext.cs +++ /dev/null @@ -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 -{ - /// - /// Context containing the common services on the . Some properties may be null until set by the . - /// - public sealed class WebAssemblyHostBuilderContext - { - /// - /// Creates a new . - /// - /// The property collection. - public WebAssemblyHostBuilderContext(IDictionary properties) - { - Properties = properties ?? throw new System.ArgumentNullException(nameof(properties)); - } - - /// - /// A central location for sharing state between components during the host building process. - /// - public IDictionary Properties { get; } - } -} \ No newline at end of file diff --git a/src/Components/Blazor/Blazor/src/Hosting/WebAssemblyHostBuilderExtensions.cs b/src/Components/Blazor/Blazor/src/Hosting/WebAssemblyHostBuilderExtensions.cs deleted file mode 100644 index 9b03c09766..0000000000 --- a/src/Components/Blazor/Blazor/src/Hosting/WebAssemblyHostBuilderExtensions.cs +++ /dev/null @@ -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 -{ - /// - /// Provides Blazor-specific support for . - /// - public static class WebAssemblyHostBuilderExtensions - { - private const string BlazorStartupKey = "Blazor.Startup"; - - /// - /// Adds services to the container. This can be called multiple times and the results will be additive. - /// - /// The to configure. - /// - /// The same instance of the for chaining. - public static IWebAssemblyHostBuilder ConfigureServices(this IWebAssemblyHostBuilder hostBuilder, Action configureDelegate) - { - return hostBuilder.ConfigureServices((context, collection) => configureDelegate(collection)); - } - - /// - /// Configures the to use the provided startup class. - /// - /// The . - /// A type that configures a Blazor application. - /// The . - 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(startup)); - - return builder; - } - - /// - /// Configures the to use the provided startup class. - /// - /// A type that configures a Blazor application. - /// The . - /// The . - public static IWebAssemblyHostBuilder UseBlazorStartup(this IWebAssemblyHostBuilder builder) - { - if (builder == null) - { - throw new ArgumentNullException(nameof(builder)); - } - - return UseBlazorStartup(builder, typeof(TStartup)); - } - } -} diff --git a/src/Components/Blazor/Blazor/src/Hosting/WebAssemblyHostExtensions.cs b/src/Components/Blazor/Blazor/src/Hosting/WebAssemblyHostExtensions.cs deleted file mode 100644 index d08162a590..0000000000 --- a/src/Components/Blazor/Blazor/src/Hosting/WebAssemblyHostExtensions.cs +++ /dev/null @@ -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 -{ - /// - /// Extension methods for . - /// - public static class WebAssemblyHostExtensions - { - /// - /// Runs the application. - /// - /// The to run. - /// - /// 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, simply starts the host - /// and allows execution to continue. - /// - 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); - } - }); - } - } -} diff --git a/src/Components/Blazor/Blazor/src/Hosting/WebAssemblyServiceFactoryAdapter.cs b/src/Components/Blazor/Blazor/src/Hosting/WebAssemblyServiceFactoryAdapter.cs deleted file mode 100644 index fcc879653a..0000000000 --- a/src/Components/Blazor/Blazor/src/Hosting/WebAssemblyServiceFactoryAdapter.cs +++ /dev/null @@ -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 : IWebAssemblyServiceFactoryAdapter - { - private IServiceProviderFactory _serviceProviderFactory; - private readonly Func _contextResolver; - private Func> _factoryResolver; - - public WebAssemblyServiceFactoryAdapter(IServiceProviderFactory serviceProviderFactory) - { - _serviceProviderFactory = serviceProviderFactory ?? throw new ArgumentNullException(nameof(serviceProviderFactory)); - } - - public WebAssemblyServiceFactoryAdapter(Func contextResolver, Func> 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); - } - } -} diff --git a/src/Components/Blazor/Blazor/src/Microsoft.AspNetCore.Blazor.csproj b/src/Components/Blazor/Blazor/src/Microsoft.AspNetCore.Blazor.csproj index e98ef09268..3a4e98a8b7 100644 --- a/src/Components/Blazor/Blazor/src/Microsoft.AspNetCore.Blazor.csproj +++ b/src/Components/Blazor/Blazor/src/Microsoft.AspNetCore.Blazor.csproj @@ -1,7 +1,7 @@ - + - netstandard2.0 + netstandard2.1 Build client-side single-page applications (SPAs) with Blazor running under WebAssembly. false @@ -9,7 +9,7 @@ - + diff --git a/src/Components/Blazor/Blazor/test/Hosting/ConventionBasedStartupTest.cs b/src/Components/Blazor/Blazor/test/Hosting/ConventionBasedStartupTest.cs deleted file mode 100644 index cbc73b79f8..0000000000 --- a/src/Components/Blazor/Blazor/test/Hosting/ConventionBasedStartupTest.cs +++ /dev/null @@ -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(() => 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(() => 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 Arguments { get; } = new List(); - - public void Configure(IComponentsApplicationBuilder app, string foo) - { - Arguments.Add(app); - Arguments.Add(foo); - } - } - } -} diff --git a/src/Components/Blazor/Blazor/test/Hosting/EntrypointInvokerTest.cs b/src/Components/Blazor/Blazor/test/Hosting/EntrypointInvokerTest.cs new file mode 100644 index 0000000000..60a3d1638b --- /dev/null +++ b/src/Components/Blazor/Blazor/test/Hosting/EntrypointInvokerTest.cs @@ -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 ? "" : string.Empty; + var paramsDecl = hasParams ? "string[] args" : string.Empty; + var returnStatement = hasReturnValue ? "return 123;" : "return;"; + var assembly = CompileToAssembly(@" +public static TaskCompletionSource ContinueTcs { get; } = new TaskCompletionSource(); + +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)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 ContinueTcs { get; } = new TaskCompletionSource(); + +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)assembly.GetType("SomeApp.Program").GetProperty("ContinueTcs").GetValue(null); + tcs.SetResult(null); + Assert.True(didMainExecute()); + } + + private static Assembly CompileToAssembly(string mainMethod, out Func 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; + } + } +} diff --git a/src/Components/Blazor/Blazor/test/Hosting/RootComponentMappingTest.cs b/src/Components/Blazor/Blazor/test/Hosting/RootComponentMappingTest.cs new file mode 100644 index 0000000000..7249402880 --- /dev/null +++ b/src/Components/Blazor/Blazor/test/Hosting/RootComponentMappingTest.cs @@ -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."); + } + } +} diff --git a/src/Components/Blazor/Blazor/test/Hosting/WebAssemblyHostBuilderTest.cs b/src/Components/Blazor/Blazor/test/Hosting/WebAssemblyHostBuilderTest.cs index 4acf6f99a3..77b6583d26 100644 --- a/src/Components/Blazor/Blazor/test/Hosting/WebAssemblyHostBuilderTest.cs +++ b/src/Components/Blazor/Blazor/test/Hosting/WebAssemblyHostBuilderTest.cs @@ -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("foo")); - builder.ConfigureServices((c, s) => s.AddSingleton(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(); - builder.ConfigureServices((c, s) => s.AddSingleton(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(); - - // Act - var ex = Assert.Throws(() => builder.UseBlazorStartup()); - - // 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("foo"); - } - } - - [Fact] - public void HostBuilder_CanCustomizeServiceFactory() - { - // Arrange - var builder = new WebAssemblyHostBuilder(); - builder.UseServiceProviderFactory(new TestServiceProviderFactory()); - - // Act - var host = builder.Build(); - - // Assert - Assert.IsType(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("key", "value"), }); // Act var host = builder.Build(); // Assert - Assert.IsType(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(); - public object GetService(Type serviceType) + // Act + var host = builder.Build(); + + // Assert + Assert.NotNull(host.Services.GetRequiredService()); + } + + [Fact] + public void Build_AddsConfigurationToServices() + { + // Arrange + var builder = WebAssemblyHostBuilder.CreateDefault(); + + builder.Configuration.AddInMemoryCollection(new[] { - if (serviceType == typeof(IWebAssemblyHost)) + new KeyValuePair("key", "value"), + }); + + // Act + var host = builder.Build(); + + // Assert + var configuration = host.Services.GetRequiredService(); + Assert.Equal("value", configuration["key"]); + } + + private static IReadOnlyList 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()); - } - else - { - return _underlyingProvider.GetService(serviceType); - } + typeof(IJSRuntime), + typeof(NavigationManager), + typeof(INavigationInterception), + typeof(ILoggerFactory), + typeof(HttpClient), + typeof(ILogger<>), + }; } } - private class TestServiceProviderFactory : IServiceProviderFactory + [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(serviceCollection); - return new TestServiceProvider(serviceCollection.BuildServiceProvider()); - } - - class TestServiceCollection : List, IServiceCollection - { - public TestServiceCollection(IEnumerable collection) - : base(collection) - { - } + Assert.Single(builder.Services, d => d.ServiceType == type); } } } diff --git a/src/Components/Blazor/Blazor/test/Hosting/WebAssemblyHostTest.cs b/src/Components/Blazor/Blazor/test/Hosting/WebAssemblyHostTest.cs index f99245e317..b838334566 100644 --- a/src/Components/Blazor/Blazor/test/Hosting/WebAssemblyHostTest.cs +++ b/src/Components/Blazor/Blazor/test/Hosting/WebAssemblyHostTest.cs @@ -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(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(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(() => 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(); + var host = builder.Build(); - public void Configure(IComponentsApplicationBuilder app, IServiceProvider services) + var disposable = host.Services.GetRequiredService(); + + 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); } } } diff --git a/src/Components/Blazor/Blazor/test/Microsoft.AspNetCore.Blazor.Tests.csproj b/src/Components/Blazor/Blazor/test/Microsoft.AspNetCore.Blazor.Tests.csproj index c93519dfbd..40c5a5b702 100644 --- a/src/Components/Blazor/Blazor/test/Microsoft.AspNetCore.Blazor.Tests.csproj +++ b/src/Components/Blazor/Blazor/test/Microsoft.AspNetCore.Blazor.Tests.csproj @@ -1,4 +1,4 @@ - + $(DefaultNetCoreTargetFramework) diff --git a/src/Components/Blazor/Build/src/Cli/Commands/ResolveRuntimeDependenciesCommand.cs b/src/Components/Blazor/Build/src/Cli/Commands/ResolveRuntimeDependenciesCommand.cs deleted file mode 100644 index d5d37bb833..0000000000 --- a/src/Components/Blazor/Build/src/Cli/Commands/ResolveRuntimeDependenciesCommand.cs +++ /dev/null @@ -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(); - - 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; - } - }); - } - } -} diff --git a/src/Components/Blazor/Build/src/Cli/Commands/WriteBootJsonCommand.cs b/src/Components/Blazor/Build/src/Cli/Commands/WriteBootJsonCommand.cs deleted file mode 100644 index dea217958c..0000000000 --- a/src/Components/Blazor/Build/src/Cli/Commands/WriteBootJsonCommand.cs +++ /dev/null @@ -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(); - - var embeddedResourcesSources = embeddedResourcesFile.HasValue() - ? File.ReadAllLines(embeddedResourcesFile.Value()) - : Array.Empty(); - - 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; - } - }); - } - } -} diff --git a/src/Components/Blazor/Build/src/Cli/Program.cs b/src/Components/Blazor/Build/src/Cli/Program.cs deleted file mode 100644 index 3bd530453f..0000000000 --- a/src/Components/Blazor/Build/src/Cli/Program.cs +++ /dev/null @@ -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; - } - } - } -} diff --git a/src/Components/Blazor/Build/src/Core/BootJsonWriter.cs b/src/Components/Blazor/Build/src/Core/BootJsonWriter.cs deleted file mode 100644 index 4d4c114158..0000000000 --- a/src/Components/Blazor/Build/src/Core/BootJsonWriter.cs +++ /dev/null @@ -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 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}"; - } - } - - /// - /// Defines the structure of a Blazor boot JSON file - /// - class BootJsonData - { - public string Main { get; } - public string EntryPoint { get; } - public IEnumerable AssemblyReferences { get; } - public IEnumerable CssReferences { get; } - public IEnumerable JsReferences { get; } - public bool LinkerEnabled { get; } - - public BootJsonData( - string entrypointAssemblyWithExtension, - string entryPoint, - IEnumerable assemblyReferences, - IEnumerable 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); - } - } - } -} diff --git a/src/Components/Blazor/Build/src/Core/EmbeddedResources/EmbeddedResourceInfo.cs b/src/Components/Blazor/Build/src/Core/EmbeddedResources/EmbeddedResourceInfo.cs deleted file mode 100644 index 97331537f2..0000000000 --- a/src/Components/Blazor/Build/src/Core/EmbeddedResources/EmbeddedResourceInfo.cs +++ /dev/null @@ -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; - } - } -} diff --git a/src/Components/Blazor/Build/src/Core/EmbeddedResources/EmbeddedResourceKind.cs b/src/Components/Blazor/Build/src/Core/EmbeddedResources/EmbeddedResourceKind.cs deleted file mode 100644 index caf322ee15..0000000000 --- a/src/Components/Blazor/Build/src/Core/EmbeddedResources/EmbeddedResourceKind.cs +++ /dev/null @@ -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 - } -} diff --git a/src/Components/Blazor/Build/src/Core/EmbeddedResources/EmbeddedResourcesProcessor.cs b/src/Components/Blazor/Build/src/Core/EmbeddedResources/EmbeddedResourcesProcessor.cs deleted file mode 100644 index 21a28597e1..0000000000 --- a/src/Components/Blazor/Build/src/Core/EmbeddedResources/EmbeddedResourcesProcessor.cs +++ /dev/null @@ -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 _knownResourceKindsByNamePrefix = new Dictionary - { - { "blazor:js:", EmbeddedResourceKind.JavaScript }, - { "blazor:css:", EmbeddedResourceKind.Css }, - { "blazor:file:", EmbeddedResourceKind.Static }, - }; - - /// - /// Finds Blazor-specific embedded resources in the specified assemblies, writes them - /// to disk, and returns a description of those resources in dependency order. - /// - /// The paths to assemblies that may contain embedded resources. - /// The path to the directory where output is being written. - /// A description of the embedded resources that were written to disk. - public static IReadOnlyList ExtractEmbeddedResources( - IEnumerable 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 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); - } -} diff --git a/src/Components/Blazor/Build/src/Core/RuntimeDependenciesResolver.cs b/src/Components/Blazor/Build/src/Core/RuntimeDependenciesResolver.cs deleted file mode 100644 index 18637753cc..0000000000 --- a/src/Components/Blazor/Build/src/Core/RuntimeDependenciesResolver.cs +++ /dev/null @@ -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 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 FindPdbs(IEnumerable 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 Results { get; } = new List(); - - internal void ResolveAssemblies() - { - var visitedAssemblies = new HashSet(); - var pendingAssemblies = new Stack(); - 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 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; - } - } -} diff --git a/src/Components/Blazor/Build/src/Microsoft.AspNetCore.Blazor.Build.csproj b/src/Components/Blazor/Build/src/Microsoft.AspNetCore.Blazor.Build.csproj index 93fa3fa9d6..8262e1139d 100644 --- a/src/Components/Blazor/Build/src/Microsoft.AspNetCore.Blazor.Build.csproj +++ b/src/Components/Blazor/Build/src/Microsoft.AspNetCore.Blazor.Build.csproj @@ -1,25 +1,26 @@ - + - $(DefaultNetCoreTargetFramework) + $(DefaultNetCoreTargetFramework);net46 + Microsoft.AspNetCore.Blazor.Build.Tasks + Microsoft.AspNetCore.Blazor.Build Build mechanism for ASP.NET Core Blazor applications. - Exe false false + false false - $(GenerateNuspecDependsOn);Publish true Microsoft.AspNetCore.Blazor.Build.nuspec - + @@ -27,16 +28,45 @@ - - - - - - - + + + + + + + + + + - + <_NetCoreFilesToCopy Include="$(OutputPath)$(DefaultNetCoreTargetFramework)\*" TargetPath="netcoreapp\" /> + <_DesktopFilesToCopy Include="$(OutputPath)net46\*" TargetPath="netfx\" /> + <_AllFilesToCopy Include="@(_NetCoreFilesToCopy);@(_DesktopFilesToCopy)" /> + + + + + + + + + diff --git a/src/Components/Blazor/Build/src/Microsoft.AspNetCore.Blazor.Build.nuspec b/src/Components/Blazor/Build/src/Microsoft.AspNetCore.Blazor.Build.nuspec index 2094d3fc5f..459fed97d0 100644 --- a/src/Components/Blazor/Build/src/Microsoft.AspNetCore.Blazor.Build.nuspec +++ b/src/Components/Blazor/Build/src/Microsoft.AspNetCore.Blazor.Build.nuspec @@ -11,7 +11,7 @@ - - + + diff --git a/src/Components/Blazor/Build/src/ReferenceBlazorBuildFromSource.props b/src/Components/Blazor/Build/src/ReferenceBlazorBuildFromSource.props new file mode 100644 index 0000000000..0bcebe22fa --- /dev/null +++ b/src/Components/Blazor/Build/src/ReferenceBlazorBuildFromSource.props @@ -0,0 +1,25 @@ + + + + + $(MSBuildThisFileDirectory)..\..\..\ + $(ComponentsRoot)Web.JS\dist\$(Configuration)\blazor.webassembly.js + $(ComponentsRoot)Web.JS\dist\$(Configuration)\blazor.webassembly.js.map + $(MSBuildThisFileDirectory)bin\$(Configuration)\tools\ + + + + + + + + + + diff --git a/src/Components/Blazor/Build/src/ReferenceFromSource.props b/src/Components/Blazor/Build/src/ReferenceFromSource.props index 8067cdc131..37e2b60e16 100644 --- a/src/Components/Blazor/Build/src/ReferenceFromSource.props +++ b/src/Components/Blazor/Build/src/ReferenceFromSource.props @@ -1,21 +1,6 @@ - - - - true - $(RepoRoot)src\Components\Web.JS\dist\$(Configuration)\blazor.*.js.* - - - - + + + + false - true - TargetFramework + TargetFramework=$(DefaultNetCoreTargetFramework) false diff --git a/src/Components/Blazor/Build/src/Tasks/BlazorCreateRootDescriptorFile.cs b/src/Components/Blazor/Build/src/Tasks/BlazorCreateRootDescriptorFile.cs new file mode 100644 index 0000000000..1aa8536856 --- /dev/null +++ b/src/Components/Blazor/Build/src/Tasks/BlazorCreateRootDescriptorFile.cs @@ -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 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); + } + } +} diff --git a/src/Components/Blazor/Build/src/Tasks/BlazorILLink.cs b/src/Components/Blazor/Build/src/Tasks/BlazorILLink.cs new file mode 100644 index 0000000000..d5dc22cde0 --- /dev/null +++ b/src/Components/Blazor/Build/src/Tasks/BlazorILLink.cs @@ -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(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); + } + } + } +} diff --git a/src/Components/Blazor/Build/src/Tasks/GenerateBlazorBootJson.cs b/src/Components/Blazor/Build/src/Tasks/GenerateBlazorBootJson.cs new file mode 100644 index 0000000000..1984de0a57 --- /dev/null +++ b/src/Components/Blazor/Build/src/Tasks/GenerateBlazorBootJson.cs @@ -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); + } + + /// + /// Defines the structure of a Blazor boot JSON file + /// +#pragma warning disable IDE1006 // Naming Styles + public class BootJsonData + { + /// + /// Gets the name of the assembly with the application entry point + /// + public string entryAssembly { get; set; } + + /// + /// Gets the closure of assemblies to be loaded by Blazor WASM. This includes the application entry assembly. + /// + public string[] assemblies { get; set; } + + /// + /// Gets a value that determines if the linker is enabled. + /// + public bool linkerEnabled { get; set; } + } +#pragma warning restore IDE1006 // Naming Styles + } +} diff --git a/src/Components/Blazor/Build/src/Tasks/GenerateTypeGranularityLinkingConfig.cs b/src/Components/Blazor/Build/src/Tasks/GenerateTypeGranularityLinkingConfig.cs new file mode 100644 index 0000000000..8a56b7fc3d --- /dev/null +++ b/src/Components/Blazor/Build/src/Tasks/GenerateTypeGranularityLinkingConfig.cs @@ -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"))); + } + } +} diff --git a/src/Components/Blazor/Build/src/Tasks/ResolveBlazorRuntimeDependencies.cs b/src/Components/Blazor/Build/src/Tasks/ResolveBlazorRuntimeDependencies.cs new file mode 100644 index 0000000000..1181ea337d --- /dev/null +++ b/src/Components/Blazor/Build/src/Tasks/ResolveBlazorRuntimeDependencies.cs @@ -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 ResolveRuntimeDependenciesCore( + string entryPoint, + IEnumerable applicationDependencies, + IEnumerable 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 CreateAssemblyLookup(IEnumerable assemblyPaths) + { + var dictionary = new Dictionary(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 FindPdbs(IEnumerable dllPaths) + { + return dllPaths + .Select(path => Path.ChangeExtension(path, "pdb")) + .Where(path => File.Exists(path)); + } + + public class AssemblyResolutionContext + { + public AssemblyResolutionContext( + AssemblyEntry entryAssembly, + Dictionary dependencies, + Dictionary bcl) + { + EntryAssembly = entryAssembly; + Dependencies = dependencies; + Bcl = bcl; + } + + public AssemblyEntry EntryAssembly { get; } + public Dictionary Dependencies { get; } + public Dictionary Bcl { get; } + + public IList Results { get; } = new List(); + + internal void ResolveAssemblies() + { + var visitedAssemblies = new HashSet(); + var pendingAssemblies = new Stack(); + 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 GetAssemblyReferences(string assemblyPath) + { + try + { + using var peReader = new PEReader(File.OpenRead(assemblyPath)); + if (!peReader.HasMetadata) + { + return Array.Empty(); // not a managed assembly + } + + var metadataReader = peReader.GetMetadataReader(); + + var references = new List(); + 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(); // 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; + } + } +} diff --git a/src/Components/Blazor/Build/src/targets/All.props b/src/Components/Blazor/Build/src/targets/All.props index 690a29fbab..d1d242f4d9 100644 --- a/src/Components/Blazor/Build/src/targets/All.props +++ b/src/Components/Blazor/Build/src/targets/All.props @@ -4,12 +4,6 @@ $(DefaultWebContentItemExcludes);wwwroot\** - - true - - - true - true diff --git a/src/Components/Blazor/Build/src/targets/All.targets b/src/Components/Blazor/Build/src/targets/All.targets index dd4fbf1b7a..6c69e85a40 100644 --- a/src/Components/Blazor/Build/src/targets/All.targets +++ b/src/Components/Blazor/Build/src/targets/All.targets @@ -6,41 +6,44 @@ - $(MSBuildThisFileDirectory)../tools/ - dotnet "$(BlazorToolsDir)Microsoft.AspNetCore.Blazor.Build.dll" + $(MSBuildThisFileDirectory)..\tools\ + <_BlazorTasksTFM Condition=" '$(MSBuildRuntimeType)' == 'Core'">netcoreapp + <_BlazorTasksTFM Condition=" '$(_BlazorTasksTFM)' == ''">netfx + $(BlazorToolsDir)$(_BlazorTasksTFM)\Microsoft.AspNetCore.Blazor.Build.Tasks.dll true + + + true + - + $(AssemblyName).blazor.config $(TargetDir)$(BlazorMetadataFileName) - - - - + + + <_BlazorConfigContent Include="$(MSBuildProjectFullPath)" /> + <_BlazorConfigContent Include="$(TargetPath)" /> + <_BlazorConfigContent Include="debug:true" Condition="'$(BlazorEnableDebugging)'=='true'" /> + + + + - - - $(GetCurrentProjectStaticWebAssetsDependsOn); - _ClearCurrentStaticWebAssetsForReferenceDiscovery - - - - - - - - - diff --git a/src/Components/Blazor/Build/src/targets/Blazor.MonoRuntime.props b/src/Components/Blazor/Build/src/targets/Blazor.MonoRuntime.props index 03f70748ff..f49c1f8f2f 100644 --- a/src/Components/Blazor/Build/src/targets/Blazor.MonoRuntime.props +++ b/src/Components/Blazor/Build/src/targets/Blazor.MonoRuntime.props @@ -1,22 +1,20 @@ - - $(MSBuildThisFileDirectory)../tools/blazor/blazor.*.js + + $(MSBuildThisFileDirectory)..\tools\blazor\blazor.webassembly.js none - --disable-opt unreachablebodies --verbose --strip-security true --exclude-feature com --exclude-feature sre -v false -c link -u link -b true - dist/ - $(BaseBlazorDistPath)_content/ - $(BaseBlazorDistPath)_framework/ - $(BaseBlazorRuntimeOutputPath)_bin/ - $(BaseBlazorRuntimeOutputPath)wasm/ - $(BaseBlazorRuntimeOutputPath) - blazor/ - wwwroot/ + --disable-opt unreachablebodies --verbose --strip-security true --exclude-feature com -v false -c link -u link -b true + dist\ + $(BaseBlazorDistPath)_content\ + $(BaseBlazorDistPath)_framework\ + $(BaseBlazorRuntimeOutputPath)_bin\ + $(BaseBlazorRuntimeOutputPath)wasm\ + wwwroot\ blazor.boot.json - $(BaseBlazorRuntimeOutputPath)$(BlazorBootJsonName) + <_BlazorBuiltInBclLinkerDescriptor>$(MSBuildThisFileDirectory)BuiltInBclLinkerDescriptor.xml diff --git a/src/Components/Blazor/Build/src/targets/Blazor.MonoRuntime.targets b/src/Components/Blazor/Build/src/targets/Blazor.MonoRuntime.targets index 69976d519b..3c7d126561 100644 --- a/src/Components/Blazor/Build/src/targets/Blazor.MonoRuntime.targets +++ b/src/Components/Blazor/Build/src/targets/Blazor.MonoRuntime.targets @@ -1,653 +1,336 @@ + + true + + + + + $(MonoBaseClassLibraryPath) + $(MonoBaseClassLibraryFacadesPath) + $(MonoWasmRuntimePath) + $(MonoWasmFrameworkPath) + + + + + $(DotNetWebAssemblyArtifactsRoot)\wasm-bcl\wasm\ + $(DotNetWebAssemblyBCLPath)\Facades\ + $(DotNetWebAssemblyArtifactsRoot)\builds\debug\ + $(DotNetWebAssemblyArtifactsRoot)\framework\ + + Condition="'@(BlazorOutputWithTargetPath)' != '' and '$(CopyBuildOutputToOutputDirectory)' == 'true' and '$(SkipCopyBuildProduct)' != 'true'"> - + - - - - - - <_BlazorResolveReferencesDidRun>true - - - - - <_BlazorStatisticsOutput Include="@(BlazorItemOutput->'%(TargetOutputPath)')" /> + <_BlazorStatisticsOutput Include="@(BlazorOutputWithTargetPath->'%(TargetOutputPath)')" /> - - <_BlazorStatisticsReportImportance Condition="'$(BlazorOutputStatistics)' == ''">normal - <_BlazorStatisticsReportImportance Condition="'$(BlazorOutputStatistics)' != ''">high - - - + - - - - - _PrepareBlazorOutputConfiguration; - _DefineBlazorCommonInputs; - _BlazorResolveOutputBinaries; - _GenerateBlazorBootJson; - - - - - - - - - - - - <_BlazorShouldLinkApplicationAssemblies Condition="$(BlazorLinkOnBuild) == 'false'"> - <_BlazorShouldLinkApplicationAssemblies Condition="$(BlazorLinkOnBuild) == 'true'">true - <_BlazorBuiltInBclLinkerDescriptor>$(MSBuildThisFileDirectory)BuiltInBclLinkerDescriptor.xml - - - - - - - $(TargetDir)$(BaseBlazorRuntimeWasmOutputPath)%(FileName)%(Extension) - WebAssembly - true - - - $(TargetDir)$(BaseBlazorJsOutputPath)%(FileName)%(Extension) - BlazorRuntime - true - + + $(BlazorRuntimeWasmOutputPath)%(FileName)%(Extension) + + + $(BaseBlazorRuntimeOutputPath)%(FileName)%(Extension) + - - <_BlazorPackageContentOutput Include="@(BlazorPackageContentFile)" Condition="%(SourcePackage) != ''"> - $(TargetDir)$(BaseBlazorPackageContentOutputPath)%(SourcePackage)\%(RecursiveDir)\%(Filename)%(Extension) - PreserveNewest + $(BaseBlazorPackageContentOutputPath)%(SourcePackage)\%(RecursiveDir)\%(Filename)%(Extension) - + + - - + + - $(IntermediateOutputPath)$(BaseBlazorIntermediateOutputPath) - $([MSBuild]::Escape($([System.IO.Path]::GetFullPath('$([System.IO.Path]::Combine('$(MSBuildProjectDirectory)', '$(BlazorIntermediateOutputPath)'))')))) - - - - - $(BlazorIntermediateOutputPath)inputs.basic.cache - - - $(BlazorIntermediateOutputPath)inputs.copylocal.txt - - - $(BlazorIntermediateOutputPath)inputs.linkerswitch.cache - - - - - $(BlazorIntermediateOutputPath)inputs.linker.cache + $(IntermediateOutputPath)blazor\ $(BlazorIntermediateOutputPath)linker.descriptor.xml + <_TypeGranularityLinkerDescriptor>$(BlazorIntermediateOutputPath)linker.typegranularityconfig.xml + $(BlazorIntermediateOutputPath)linker/ - - $(BlazorIntermediateOutputPath)linked.assemblies.txt - - - - - $(BlazorIntermediateOutputPath)resolvedassemblies/ - - - $(BlazorIntermediateOutputPath)resolved.assemblies.txt - - - - - $(BlazorIntermediateOutputPath) - - $(BlazorBootJsonIntermediateOutputDir)$(BlazorBootJsonName) + $(BlazorIntermediateOutputPath)$(BlazorBootJsonName) - - $(BlazorIntermediateOutputPath)inputs.bootjson.cache - - - $(BlazorIntermediateOutputPath)resolve-dependencies.txt - - - $(BlazorIntermediateOutputPath)bootjson-references.txt - - - $(BlazorIntermediateOutputPath)embedded.resources.txt + <_BlazorLinkerOutputCache>$(BlazorIntermediateOutputPath)linker.output + <_BlazorApplicationAssembliesCacheFile>$(BlazorIntermediateOutputPath)unlinked.output - - $(TargetDir)$(BaseBlazorRuntimeBinOutputPath) - + + <_WebAssemblyBCLFolder Include=" + $(DotNetWebAssemblyBCLPath); + $(DotNetWebAssemblyBCLFacadesPath); + $(DotNetWebAssemblyFrameworkPath)" /> + + <_WebAssemblyBCLAssembly Include="%(_WebAssemblyBCLFolder.Identity)*.dll" /> + + + + + + <_BlazorManagedRuntimeAssemby Include="@(RuntimeCopyLocalItems)" /> + + + <_BlazorUserRuntimeAssembly Include="@(ReferencePath->WithMetadataValue('CopyLocal', 'true'))" /> + <_BlazorUserRuntimeAssembly Include="@(ReferenceDependencyPaths->WithMetadataValue('CopyLocal', 'true'))" /> + + <_BlazorManagedRuntimeAssemby Include="@(_BlazorUserRuntimeAssembly)" /> + <_BlazorManagedRuntimeAssemby Include="@(IntermediateAssembly)" /> + - - - - - - - - - <_BlazorDependencyInput Include="@(ReferenceCopyLocalPaths->WithMetadataValue('Extension','.dll')->'%(FullPath)')" /> - + + - <_BlazorCommonInput Include="@(IntermediateAssembly)" /> - <_BlazorCommonInput Include="@(_BlazorDependencyInput)" /> - <_BlazorCommonInput Include="$(_BlazorShouldLinkApplicationAssemblies)" /> - <_BlazorCommonInput Include="$(BlazorEnableDebugging)" /> - <_BlazorLinkingOption Condition="'$(_BlazorShouldLinkApplicationAssemblies)' == ''" Include="false" /> - <_BlazorLinkingOption Condition="'$(_BlazorShouldLinkApplicationAssemblies)' != ''" Include="true" /> + + <_BlazorCopyLocalPaths Include="@(ReferenceCopyLocalPaths)" /> + <_BlazorCopyLocalPaths Remove="@(_BlazorManagedRuntimeAssemby)" /> + + + true + $(BlazorRuntimeBinOutputPath)%(_BlazorCopyLocalPaths.DestinationSubDirectory)%(FileName)%(Extension) + %(_BlazorCopyLocalPaths.DestinationSubDirectory)%(FileName)%(Extension) + + + + true + $(BlazorRuntimeBinOutputPath)%(FileName)%(Extension) + %(FileName)%(Extension) + - - - - - - - - - - - - - - - - - - - - - - <_CollectLinkerOutputsDependsOn> - _GenerateLinkerDescriptor; - _CollectBlazorLinkerDescriptors; - _LinkBlazorApplication - - - - - - + Name="_ResolveBlazorOutputsWhenLinked" + Condition="'$(BlazorLinkOnBuild)' == 'true'" + DependsOnTargets="_PrepareBlazorLinkerInputs;_GenerateBlazorLinkerDescriptor;_GenerateTypeGranularLinkerDescriptor;_LinkBlazorApplication"> + + + + + + - - $(BlazorRuntimeBinOutputPath)%(FileName)%(Extension) - Assembly - true - - - $(BlazorRuntimeBinOutputPath)%(FileName)%(Extension) - Pdb - - + <_BlazorRuntimeCopyLocalItems Include="@(RuntimeCopyLocalItems)" /> + + + <_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'" /> - + + Condition="'@(BlazorLinkerDescriptor)' == ''"> + + + + - <_PrepareLinkerDescriptorAssemblyLine Include="@(IntermediateAssembly->'%(FileName)')" /> - <_GeneratedLinkerDescriptorLine Include="<linker>" /> - <_GeneratedLinkerDescriptorLine Include="@(_PrepareLinkerDescriptorAssemblyLine->'<assembly fullname="%(Identity)" />')" /> - <_GeneratedLinkerDescriptorLine Include="</linker>" /> - - - - - - - - - - - + + - - - <_BlazorLinkerInput Include="@(IntermediateAssembly)" /> - <_BlazorLinkerInput Include="@(_BlazorDependencyInput)" /> - <_BlazorLinkerInput Include="@(BlazorLinkerDescriptor)" /> - <_BlazorLinkerInput Include="$(AdditionalMonoLinkerOptions)" /> - - - - - - - - - - - - + + + + + + + + + + + + - - - <_MonoBaseClassLibraryFolder Include="$(MonoBaseClassLibraryPath);$(MonoBaseClassLibraryFacadesPath);$(MonoWasmFrameworkPath)" /> - <_BlazorAssembliesToLink Include="@(_BlazorDependencyInput->'-a "%(Identity)"')" /> - <_BlazorAssembliesToLink Include="@(IntermediateAssembly->'-a "%(FullPath)"')" /> - <_BlazorFolderLookupPaths Include="@(_MonoBaseClassLibraryFolder->'-d "%(Identity)"')" /> - <_BlazorAssemblyDescriptorFiles - Include="@(BlazorLinkerDescriptor->'-x "%(FullPath)"')" Condition="'@(BlazorLinkerDescriptor)' != ''" /> - + Inputs="$(ProjectAssetsFile); + @(_BlazorManagedRuntimeAssemby); + @(BlazorLinkerDescriptor); + $(MSBuildAllProjects)" + Outputs="$(_BlazorLinkerOutputCache)"> <_BlazorLinkerAdditionalOptions>-l $(MonoLinkerI18NAssemblies) $(AdditionalMonoLinkerOptions) - - - - - - - - <_BlazorLinkerOutput Include="$(BlazorIntermediateLinkerOutputPath)*.dll" /> - <_BlazorLinkerOutput Include="$(BlazorIntermediateLinkerOutputPath)*.pdb" /> + <_OldLinkedFile Include="$(BlazorIntermediateLinkerOutputPath)*.dll" /> + <_OldLinkedFile Include="$(BlazorIntermediateLinkerOutputPath)*.pdb" /> + + - + + <_DotNetHostDirectory>$(NetCoreRoot) + <_DotNetHostFileName>dotnet + <_DotNetHostFileName Condition=" '$(OS)' == 'Windows_NT' ">dotnet.exe + + + - - - - + <_LinkerResult Include="$(BlazorIntermediateLinkerOutputPath)*.dll" /> + <_LinkerResult Include="$(BlazorIntermediateLinkerOutputPath)*.pdb" Condition="'$(BlazorEnableDebugging)' == 'true'" /> + + - - - - <_CollectResolvedAssembliesDependsOn> - _ResolveBlazorApplicationAssemblies; - _ReadResolvedBlazorApplicationAssemblies; - _IntermediateCopyBlazorApplicationAssemblies; - _TouchBlazorApplicationAssemblies - - + + + + + Name="_ResolveBlazorRuntimeDependencies" + Inputs="$(ProjectAssetsFile); + @(IntermediateAssembly); + @(_BlazorManagedRuntimeAssemby)" + Outputs="$(_BlazorApplicationAssembliesCacheFile)"> + + + + + + - - $(BlazorRuntimeBinOutputPath)%(FileName)%(Extension) - Assembly - true - - - $(BlazorRuntimeBinOutputPath)%(FileName)%(Extension) - Pdb - - + - - - - <_ReferencesArg Condition="'@(_BlazorDependencyInput)' != ''">--references "$(BlazorResolveDependenciesFilePath)" - <_BclParameter>--base-class-library "$(MonoBaseClassLibraryPath)" --base-class-library "$(MonoBaseClassLibraryFacadesPath)" --base-class-library "$(MonoWasmFrameworkPath)" - - - - - - - - - - - - - - - - <_IntermediateResolvedRuntimeDependencies Include="@(_BlazorResolvedRuntimeDependencies->'$(BlazorIntermediateResolvedApplicationAssembliesOutputPath)%(FileName)%(Extension)')" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - <_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'))" /> - - <_LinkerEnabledFlag Condition="'$(_BlazorShouldLinkApplicationAssemblies)' != ''">--linker-enabled - <_ReferencesArg Condition="'@(_AppReferences)' != ''">--references "$(BlazorBootJsonReferencesFilePath)" - <_EmbeddedResourcesArg Condition="'@(_UnlinkedAppReferencesPaths)' != ''">--embedded-resources "$(BlazorEmbeddedResourcesConfigFilePath)" - - + - - - - - - <_BlazorBootJson Include="$(BlazorBootJsonIntermediateOutputPath)" /> - <_BlazorBootJsonEmbeddedContentFile Include="$(BlazorBootJsonIntermediateOutputDir)_content\**\*.*" /> - - $(TargetDir)$(BlazorBootJsonOutputPath) - BootJson - - - $(TargetDir)dist/_content/%(RecursiveDir)%(FileName)%(Extension) - + + - - diff --git a/src/Components/Blazor/Build/src/targets/Publish.targets b/src/Components/Blazor/Build/src/targets/Publish.targets index e431ad1272..7cb7e0ad23 100644 --- a/src/Components/Blazor/Build/src/targets/Publish.targets +++ b/src/Components/Blazor/Build/src/targets/Publish.targets @@ -26,9 +26,8 @@ - <_BlazorGCTPDIDistFiles Include="@(BlazorItemOutput->'%(TargetOutputPath)')" /> - <_BlazorGCTPDI Include="@(_BlazorGCTPDIDistFiles)"> - $(BlazorPublishDistDir)$([MSBuild]::MakeRelative('$(TargetDir)dist\', %(Identity))) + <_BlazorGCTPDI Include="%(BlazorOutputWithTargetPath.Identity)"> + $(AssemblyName)\%(TargetOutputPath) @@ -41,8 +40,17 @@ <_BlazorConfigPath>$(OutDir)$(AssemblyName).blazor.config - - + + + <_BlazorPublishConfigContent Include="." /> + <_BlazorPublishConfigContent Include="$(AssemblyName)/" /> + + + diff --git a/src/Components/Blazor/Build/src/targets/StaticWebAssets.targets b/src/Components/Blazor/Build/src/targets/StaticWebAssets.targets new file mode 100644 index 0000000000..d547f500b1 --- /dev/null +++ b/src/Components/Blazor/Build/src/targets/StaticWebAssets.targets @@ -0,0 +1,37 @@ + + + + + $(ResolveStaticWebAssetsInputsDependsOn); + _RemoveBlazorCurrentProjectAssetsFromStaticWebAssets; + + + + $(GetCurrentProjectStaticWebAssetsDependsOn); + _RemoveBlazorCurrentProjectAssetsFromStaticWebAssets; + + + + + + + + + + + + + + + <_StandaloneExternalPublishStaticWebAsset Include="@(_ExternalPublishStaticWebAsset)" Condition="'%(RelativePath)' != ''"> + $([MSBuild]::MakeRelative('$(MSBuildProjectDirectory)', '$([MSBuild]::NormalizePath('$([System.Text.RegularExpressions.Regex]::Replace('%(RelativePath)','^wwwroot\\?\/?(.*)','$(BlazorPublishDistDir)$1'))'))')) + + + + + + + + + diff --git a/src/Components/Blazor/Build/test/BlazorCreateRootDescriptorFileTest.cs b/src/Components/Blazor/Build/test/BlazorCreateRootDescriptorFileTest.cs new file mode 100644 index 0000000000..4470546cf0 --- /dev/null +++ b/src/Components/Blazor/Build/test/BlazorCreateRootDescriptorFileTest.cs @@ -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); + } + } +} diff --git a/src/Components/Blazor/Build/test/BootJsonWriterTest.cs b/src/Components/Blazor/Build/test/BootJsonWriterTest.cs index 3632a082b7..1e2d89b573 100644 --- a/src/Components/Blazor/Build/test/BootJsonWriterTest.cs +++ b/src/Components/Blazor/Build/test/BootJsonWriterTest.cs @@ -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(), linkerEnabled: true); // Assert - var parsedContent = JsonConvert.DeserializeObject(content); - Assert.Equal("MyApp.Entrypoint.dll", parsedContent["main"].Value()); - Assert.Equal("MyNamespace.MyType::MyMethod", parsedContent["entryPoint"].Value()); - Assert.Equal(assemblyReferences, parsedContent["assemblyReferences"].Values()); - } - - [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(content); - Assert.Equal( - new[] { "css/first.css", "css/second.css" }, - parsedContent["cssReferences"].Values()); - Assert.Equal( - new[] { "javascript/first.js", "javascript/second.js" }, - parsedContent["jsReferences"].Values()); + Assert.Equal(assemblyReferences[i], assembliesElement[i].GetString()); + } + Assert.True(rootElement.GetProperty("linkerEnabled").GetBoolean()); } } } diff --git a/src/Components/Blazor/Build/test/BuildIntegrationTests/Assert.cs b/src/Components/Blazor/Build/test/BuildIntegrationTests/Assert.cs new file mode 100644 index 0000000000..8d0aa4b6da --- /dev/null +++ b/src/Components/Blazor/Build/test/BuildIntegrationTests/Assert.cs @@ -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()); + } + } + + 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 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 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 warnings) + : base(result) + { + Warnings = warnings.ToList(); + } + + public List 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}."; + } + } +} diff --git a/src/Components/Blazor/Build/test/BuildIntegrationTests/BuildIncrementalismTest.cs b/src/Components/Blazor/Build/test/BuildIntegrationTests/BuildIncrementalismTest.cs new file mode 100644 index 0000000000..73d8645029 --- /dev/null +++ b/src/Components/Blazor/Build/test/BuildIntegrationTests/BuildIncrementalismTest.cs @@ -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]); + } + } + } + } +} diff --git a/src/Components/Blazor/Build/test/BuildIntegrationTests/BuildIntegrationTest.cs b/src/Components/Blazor/Build/test/BuildIntegrationTests/BuildIntegrationTest.cs new file mode 100644 index 0000000000..f3148b1a0b --- /dev/null +++ b/src/Components/Blazor/Build/test/BuildIntegrationTests/BuildIntegrationTest.cs @@ -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( +@" + false +"); + + 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( +@" + + $(DefineConstants);REFERENCE_classlibrarywithsatelliteassemblies + + + +"); + + 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( +@" + + false + $(DefineConstants);REFERENCE_classlibrarywithsatelliteassemblies + + + +"); + + 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\""); + } + } +} diff --git a/src/Components/Blazor/Build/test/BuildIntegrationTests/FileThumbPrint.cs b/src/Components/Blazor/Build/test/BuildIntegrationTests/FileThumbPrint.cs new file mode 100644 index 0000000000..58b5499e8b --- /dev/null +++ b/src/Components/Blazor/Build/test/BuildIntegrationTests/FileThumbPrint.cs @@ -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 + { + 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}"; + } + + /// + /// Returns a list of thumbprints for all files (recursive) in the specified directory, sorted by file paths. + /// + public static List 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(); + foreach (var file in files) + { + var thumbprint = Create(file); + thumbprintLookup.Add(thumbprint); + } + + thumbprintLookup.Sort(Comparer.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(); + } +} diff --git a/src/Components/Blazor/Build/test/BuildIntegrationTests/MSBuildProcessManager.cs b/src/Components/Blazor/Build/test/BuildIntegrationTests/MSBuildProcessManager.cs new file mode 100644 index 0000000000..b7e16ca072 --- /dev/null +++ b/src/Components/Blazor/Build/test/BuildIntegrationTests/MSBuildProcessManager.cs @@ -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 DotnetMSBuild( + ProjectDirectory project, + string target = "Build", + string args = null) + { + var buildArgumentList = new List + { + // 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 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 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(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 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 CaptureDump(Process process, TimeSpan? timeout, StringBuilder diagnostics) + { + var metadataAttributes = Assembly.GetExecutingAssembly() + .GetCustomAttributes(); + + 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(); + + 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; } + } + } +} diff --git a/src/Components/Blazor/Build/test/BuildIntegrationTests/MSBuildResult.cs b/src/Components/Blazor/Build/test/BuildIntegrationTests/MSBuildResult.cs new file mode 100644 index 0000000000..9a83df922b --- /dev/null +++ b/src/Components/Blazor/Build/test/BuildIntegrationTests/MSBuildResult.cs @@ -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; } + } + +} diff --git a/src/Components/Blazor/Build/test/BuildIntegrationTests/ProjectDirectory.cs b/src/Components/Blazor/Build/test/BuildIntegrationTests/ProjectDirectory.cs new file mode 100644 index 0000000000..e50b750ae4 --- /dev/null +++ b/src/Components/Blazor/Build/test/BuildIntegrationTests/ProjectDirectory.cs @@ -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())) + { + 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 = +$@" + + {repoRoot} + +"; + File.WriteAllText(Path.Combine(projectDestination, "Before.Directory.Build.props"), beforeDirectoryPropsContent); + + new List { "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("", 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() + .FirstOrDefault(f => f.Key == key) + ?.Value; + } + } +} diff --git a/src/Components/Blazor/Build/test/BuildIntegrationTests/ProjectDirectoryTest.cs b/src/Components/Blazor/Build/test/BuildIntegrationTests/ProjectDirectoryTest.cs new file mode 100644 index 0000000000..23badb4296 --- /dev/null +++ b/src/Components/Blazor/Build/test/BuildIntegrationTests/ProjectDirectoryTest.cs @@ -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); + } + } +} diff --git a/src/Components/Blazor/Build/test/BuildIntegrationTests/PublishIntegrationTest.cs b/src/Components/Blazor/Build/test/BuildIntegrationTests/PublishIntegrationTest.cs new file mode 100644 index 0000000000..3556f119f4 --- /dev/null +++ b/src/Components/Blazor/Build/test/BuildIntegrationTests/PublishIntegrationTest.cs @@ -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( +@" + false +"); + + 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( +@" + + $(DefineConstants);REFERENCE_classlibrarywithsatelliteassemblies + + + +"); + + 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"); + } + } +} diff --git a/src/Components/Blazor/Build/test/RuntimeDependenciesResolverTest.cs b/src/Components/Blazor/Build/test/RuntimeDependenciesResolverTest.cs index 5838d419d7..951b1a61df 100644 --- a/src/Components/Blazor/Build/test/RuntimeDependenciesResolverTest.cs +++ b/src/Components/Blazor/Build/test/RuntimeDependenciesResolverTest.cs @@ -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, diff --git a/src/Components/Blazor/Build/testassets/Directory.Build.props b/src/Components/Blazor/Build/testassets/Directory.Build.props new file mode 100644 index 0000000000..cf4b11d0cf --- /dev/null +++ b/src/Components/Blazor/Build/testassets/Directory.Build.props @@ -0,0 +1,31 @@ + + + + + $([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), global.json))\ + $(RepoRoot)src\Components\ + $(ComponentsRoot)Blazor\Build\src\ + $(BlazorBuildRoot)ReferenceBlazorBuildFromSource.props + + + netcoreapp3.1 + + false + false + + + + + + + + + + + diff --git a/src/Components/benchmarkapps/Directory.Build.props b/src/Components/Blazor/Build/testassets/Directory.Build.targets similarity index 100% rename from src/Components/benchmarkapps/Directory.Build.props rename to src/Components/Blazor/Build/testassets/Directory.Build.targets diff --git a/src/Components/Blazor/Build/testassets/blazorhosted/Program.cs b/src/Components/Blazor/Build/testassets/blazorhosted/Program.cs new file mode 100644 index 0000000000..e2efcc0c74 --- /dev/null +++ b/src/Components/Blazor/Build/testassets/blazorhosted/Program.cs @@ -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)); + } + } +} diff --git a/src/Components/Blazor/Build/testassets/blazorhosted/blazorhosted.csproj b/src/Components/Blazor/Build/testassets/blazorhosted/blazorhosted.csproj new file mode 100644 index 0000000000..5a89588d8c --- /dev/null +++ b/src/Components/Blazor/Build/testassets/blazorhosted/blazorhosted.csproj @@ -0,0 +1,12 @@ + + + + netcoreapp3.1 + true + + + + + + + diff --git a/src/Components/Blazor/Build/testassets/classlibrarywithsatelliteassemblies/Class1.cs b/src/Components/Blazor/Build/testassets/classlibrarywithsatelliteassemblies/Class1.cs new file mode 100644 index 0000000000..944699cdb3 --- /dev/null +++ b/src/Components/Blazor/Build/testassets/classlibrarywithsatelliteassemblies/Class1.cs @@ -0,0 +1,12 @@ +using System; + +namespace classlibrarywithsatelliteassemblies +{ + public class Class1 + { + public static void Test() + { + GC.KeepAlive(typeof(Microsoft.CodeAnalysis.CSharp.CSharpCompilation)); + } + } +} \ No newline at end of file diff --git a/src/Components/Blazor/Build/testassets/classlibrarywithsatelliteassemblies/classlibrarywithsatelliteassemblies.csproj b/src/Components/Blazor/Build/testassets/classlibrarywithsatelliteassemblies/classlibrarywithsatelliteassemblies.csproj new file mode 100644 index 0000000000..7081842748 --- /dev/null +++ b/src/Components/Blazor/Build/testassets/classlibrarywithsatelliteassemblies/classlibrarywithsatelliteassemblies.csproj @@ -0,0 +1,13 @@ + + + + netstandard2.1 + 3.0 + + + + + + + + diff --git a/src/Components/Blazor/Build/testassets/razorclasslibrary/RazorClassLibrary.csproj b/src/Components/Blazor/Build/testassets/razorclasslibrary/RazorClassLibrary.csproj new file mode 100644 index 0000000000..94e866815d --- /dev/null +++ b/src/Components/Blazor/Build/testassets/razorclasslibrary/RazorClassLibrary.csproj @@ -0,0 +1,8 @@ + + + + netstandard2.1 + 3.0 + + + diff --git a/src/Components/Blazor/Build/testassets/razorclasslibrary/wwwroot/styles.css b/src/Components/Blazor/Build/testassets/razorclasslibrary/wwwroot/styles.css new file mode 100644 index 0000000000..5f282702bb --- /dev/null +++ b/src/Components/Blazor/Build/testassets/razorclasslibrary/wwwroot/styles.css @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/Components/Blazor/Build/testassets/razorclasslibrary/wwwroot/wwwroot/exampleJsInterop.js b/src/Components/Blazor/Build/testassets/razorclasslibrary/wwwroot/wwwroot/exampleJsInterop.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/Components/Blazor/Build/testassets/standalone/App.razor b/src/Components/Blazor/Build/testassets/standalone/App.razor new file mode 100644 index 0000000000..eba23da9b5 --- /dev/null +++ b/src/Components/Blazor/Build/testassets/standalone/App.razor @@ -0,0 +1,8 @@ + + + + + +

Sorry, there's nothing at this address.

+
+
diff --git a/src/Components/Blazor/Build/testassets/standalone/Pages/Index.razor b/src/Components/Blazor/Build/testassets/standalone/Pages/Index.razor new file mode 100644 index 0000000000..16dac31925 --- /dev/null +++ b/src/Components/Blazor/Build/testassets/standalone/Pages/Index.razor @@ -0,0 +1,5 @@ +@page "/" + +

Hello, world!

+ +Welcome to your new app. diff --git a/src/Components/Blazor/Build/testassets/standalone/Program.cs b/src/Components/Blazor/Build/testassets/standalone/Program.cs new file mode 100644 index 0000000000..3e46e63316 --- /dev/null +++ b/src/Components/Blazor/Build/testassets/standalone/Program.cs @@ -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 + } + } +} diff --git a/src/Components/Blazor/Build/testassets/standalone/_Imports.razor b/src/Components/Blazor/Build/testassets/standalone/_Imports.razor new file mode 100644 index 0000000000..129b440e86 --- /dev/null +++ b/src/Components/Blazor/Build/testassets/standalone/_Imports.razor @@ -0,0 +1,2 @@ +@using Microsoft.AspNetCore.Components.Routing +@using standalone diff --git a/src/Components/Blazor/Build/testassets/standalone/standalone.csproj b/src/Components/Blazor/Build/testassets/standalone/standalone.csproj new file mode 100644 index 0000000000..1b13eb3d53 --- /dev/null +++ b/src/Components/Blazor/Build/testassets/standalone/standalone.csproj @@ -0,0 +1,20 @@ + + + + + netstandard2.1 + 3.0 + + + + + + + + + + + + + + diff --git a/src/Components/Blazor/Build/testassets/standalone/wwwroot/index.html b/src/Components/Blazor/Build/testassets/standalone/wwwroot/index.html new file mode 100644 index 0000000000..85994d6e89 --- /dev/null +++ b/src/Components/Blazor/Build/testassets/standalone/wwwroot/index.html @@ -0,0 +1,24 @@ + + + + + + + standalone + + + + + + + Loading... + +
+ An unhandled error has occurred. + Reload + 🗙 +
+ + + + diff --git a/src/Components/Blazor/DevServer/src/Microsoft.AspNetCore.Blazor.DevServer.csproj b/src/Components/Blazor/DevServer/src/Microsoft.AspNetCore.Blazor.DevServer.csproj index 53514cf36c..4ccf78104b 100644 --- a/src/Components/Blazor/DevServer/src/Microsoft.AspNetCore.Blazor.DevServer.csproj +++ b/src/Components/Blazor/DevServer/src/Microsoft.AspNetCore.Blazor.DevServer.csproj @@ -11,15 +11,14 @@ Development server for use when building Blazor applications. false + + + true - - - - diff --git a/src/Components/Blazor/Directory.Build.targets b/src/Components/Blazor/Directory.Build.targets index 178608d3e5..e1a17eb9ca 100644 --- a/src/Components/Blazor/Directory.Build.targets +++ b/src/Components/Blazor/Directory.Build.targets @@ -4,4 +4,5 @@ $(PackageVersion) + diff --git a/src/Components/Blazor/Http/src/Microsoft.AspNetCore.Blazor.HttpClient.csproj b/src/Components/Blazor/Http/src/Microsoft.AspNetCore.Blazor.HttpClient.csproj index 9c5974d9d6..9d6deb6173 100644 --- a/src/Components/Blazor/Http/src/Microsoft.AspNetCore.Blazor.HttpClient.csproj +++ b/src/Components/Blazor/Http/src/Microsoft.AspNetCore.Blazor.HttpClient.csproj @@ -4,6 +4,7 @@ netstandard2.0 Provides experimental support for using System.Text.Json with HttpClient. Intended for use with Blazor running under WebAssembly. false + false diff --git a/src/Components/Blazor/Mono.WebAssembly.Interop/src/InternalCalls.cs b/src/Components/Blazor/Mono.WebAssembly.Interop/src/InternalCalls.cs new file mode 100644 index 0000000000..60c0cdc429 --- /dev/null +++ b/src/Components/Blazor/Mono.WebAssembly.Interop/src/InternalCalls.cs @@ -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 +{ + /// + /// Methods that map to the functions compiled into the Mono WebAssembly runtime, + /// as defined by 'mono_add_internal_call' calls in driver.c + /// + 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(out string exception, string functionIdentifier, T0 arg0, T1 arg1, T2 arg2); + } +} diff --git a/src/Components/Blazor/Mono.WebAssembly.Interop/src/Mono.WebAssembly.Interop.csproj b/src/Components/Blazor/Mono.WebAssembly.Interop/src/Mono.WebAssembly.Interop.csproj new file mode 100644 index 0000000000..ea714b2d92 --- /dev/null +++ b/src/Components/Blazor/Mono.WebAssembly.Interop/src/Mono.WebAssembly.Interop.csproj @@ -0,0 +1,17 @@ + + + + netstandard2.1 + Abstractions and features for interop between Mono WebAssembly and JavaScript code. + wasm;javascript;interop + true + true + true + false + + + + + + + diff --git a/src/Components/Blazor/Mono.WebAssembly.Interop/src/MonoWebAssemblyJSRuntime.cs b/src/Components/Blazor/Mono.WebAssembly.Interop/src/MonoWebAssemblyJSRuntime.cs new file mode 100644 index 0000000000..654263a123 --- /dev/null +++ b/src/Components/Blazor/Mono.WebAssembly.Interop/src/MonoWebAssemblyJSRuntime.cs @@ -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 +{ + /// + /// Provides methods for invoking JavaScript functions for applications running + /// on the Mono WebAssembly runtime. + /// + public class MonoWebAssemblyJSRuntime : JSInProcessRuntime + { + /// + /// Gets the used to perform operations using . + /// + private static MonoWebAssemblyJSRuntime Instance { get; set; } + + /// + /// Initializes the to be used to perform operations using . + /// + /// The instance. + protected static void Initialize(MonoWebAssemblyJSRuntime jsRuntime) + { + if (Instance != null) + { + throw new InvalidOperationException("MonoWebAssemblyJSRuntime has already been initialized."); + } + + Instance = jsRuntime ?? throw new ArgumentNullException(nameof(jsRuntime)); + } + + /// + 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; + } + + /// + 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 + + /// + /// Invokes the JavaScript function registered with the specified identifier. + /// + /// The .NET type corresponding to the function's return value type. + /// The identifier used when registering the target function. + /// The result of the function invocation. + public TRes InvokeUnmarshalled(string identifier) + => InvokeUnmarshalled(identifier, null, null, null); + + /// + /// Invokes the JavaScript function registered with the specified identifier. + /// + /// The type of the first argument. + /// The .NET type corresponding to the function's return value type. + /// The identifier used when registering the target function. + /// The first argument. + /// The result of the function invocation. + public TRes InvokeUnmarshalled(string identifier, T0 arg0) + => InvokeUnmarshalled(identifier, arg0, null, null); + + /// + /// Invokes the JavaScript function registered with the specified identifier. + /// + /// The type of the first argument. + /// The type of the second argument. + /// The .NET type corresponding to the function's return value type. + /// The identifier used when registering the target function. + /// The first argument. + /// The second argument. + /// The result of the function invocation. + public TRes InvokeUnmarshalled(string identifier, T0 arg0, T1 arg1) + => InvokeUnmarshalled(identifier, arg0, arg1, null); + + /// + /// Invokes the JavaScript function registered with the specified identifier. + /// + /// The type of the first argument. + /// The type of the second argument. + /// The type of the third argument. + /// The .NET type corresponding to the function's return value type. + /// The identifier used when registering the target function. + /// The first argument. + /// The second argument. + /// The third argument. + /// The result of the function invocation. + public TRes InvokeUnmarshalled(string identifier, T0 arg0, T1 arg1, T2 arg2) + { + var result = InternalCalls.InvokeJSUnmarshalled(out var exception, identifier, arg0, arg1, arg2); + return exception != null + ? throw new JSException(exception) + : result; + } + + #endregion + } +} diff --git a/src/Components/Blazor/Server/src/Microsoft.AspNetCore.Blazor.Server.csproj b/src/Components/Blazor/Server/src/Microsoft.AspNetCore.Blazor.Server.csproj index ab6c3b6ee6..7596c1a8cb 100644 --- a/src/Components/Blazor/Server/src/Microsoft.AspNetCore.Blazor.Server.csproj +++ b/src/Components/Blazor/Server/src/Microsoft.AspNetCore.Blazor.Server.csproj @@ -4,6 +4,9 @@ $(DefaultNetCoreTargetFramework) Runtime server features for ASP.NET Core Blazor applications. false + false + + true @@ -11,11 +14,6 @@ - - - - - diff --git a/src/Components/Blazor/Server/src/MonoDebugProxy/BlazorMonoDebugProxyAppBuilderExtensions.cs b/src/Components/Blazor/Server/src/MonoDebugProxy/BlazorMonoDebugProxyAppBuilderExtensions.cs index 533cd99399..cbe0fe363a 100644 --- a/src/Components/Blazor/Server/src/MonoDebugProxy/BlazorMonoDebugProxyAppBuilderExtensions.cs +++ b/src/Components/Blazor/Server/src/MonoDebugProxy/BlazorMonoDebugProxyAppBuilderExtensions.cs @@ -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 /// public static class BlazorMonoDebugProxyAppBuilderExtensions { + private static JsonSerializerOptions JsonOptions = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + PropertyNameCaseInsensitive = true, + IgnoreNullValues = true + }; + + private static string DefaultDebuggerHost = "http://localhost:9222"; + /// /// 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 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 $@"

Press Win+R and enter the following:

-

chrome --remote-debugging-port=9222 --user-data-dir=""{profilePath}"" {appRootUrl}

"; +

chrome --remote-debugging-port={debuggerPort} --user-data-dir=""{profilePath}"" {appRootUrl}

"; } else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { return $@"

In a terminal window execute the following:

-

google-chrome --remote-debugging-port=9222 --user-data-dir={profilePath} {appRootUrl}

"; +

google-chrome --remote-debugging-port={debuggerPort} --user-data-dir={profilePath} {appRootUrl}

"; } else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { return $@"

Execute the following:

-

open /Applications/Google\ Chrome.app --args --remote-debugging-port=9222 --user-data-dir={profilePath} {appRootUrl}

"; +

open /Applications/Google\ Chrome.app --args --remote-debugging-port={debuggerPort} --user-data-dir={profilePath} {appRootUrl}

"; } 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 $@"

Press Win+R and enter the following:

-

msedge --remote-debugging-port=9222 --user-data-dir=""{profilePath}"" {appRootUrl}

"; +

msedge --remote-debugging-port={debugggerPort} --user-data-dir=""{profilePath}"" --no-first-run {appRootUrl}

"; } else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { return $@"

In a terminal window execute the following:

-

open /Applications/Microsoft\ Edge\ Dev.app --args --remote-debugging-port=9222 --user-data-dir={profilePath} {appRootUrl}

"; +

open /Applications/Microsoft\ Edge\ Dev.app --args --remote-debugging-port={debugggerPort} --user-data-dir={profilePath} {appRootUrl}

"; } else { @@ -196,17 +289,24 @@ namespace Microsoft.AspNetCore.Builder } } - private static async Task> GetOpenedBrowserTabs(string debuggerHost) + private static async Task GetBrowserVersionInfoAsync() { - using (var httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(5) }) - { - var jsonResponse = await httpClient.GetStringAsync($"{debuggerHost}/json"); - return JsonConvert.DeserializeObject(jsonResponse); - } + using var httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(5) }; + var debuggerHost = GetDebuggerHost(); + return await httpClient.GetStringAsync($"{debuggerHost}/json/version"); + } + + private static async Task> GetOpenedBrowserTabs() + { + using var httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(5) }; + var debuggerHost = GetDebuggerHost(); + var jsonResponse = await httpClient.GetStringAsync($"{debuggerHost}/json"); + return JsonSerializer.Deserialize(jsonResponse, JsonOptions); } class BrowserTab { + public string Id { get; set; } public string Type { get; set; } public string Url { get; set; } public string Title { get; set; } diff --git a/src/Components/Blazor/Server/src/MonoDebugProxy/ws-proxy/DebugStore.cs b/src/Components/Blazor/Server/src/MonoDebugProxy/ws-proxy/DebugStore.cs index ee28bd94b7..e1e9b7392b 100644 --- a/src/Components/Blazor/Server/src/MonoDebugProxy/ws-proxy/DebugStore.cs +++ b/src/Components/Blazor/Server/src/MonoDebugProxy/ws-proxy/DebugStore.cs @@ -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) diff --git a/src/Components/Blazor/Server/src/MonoDebugProxy/ws-proxy/MonoProxy.cs b/src/Components/Blazor/Server/src/MonoDebugProxy/ws-proxy/MonoProxy.cs index 8c440da1ce..eb4cf65b50 100644 --- a/src/Components/Blazor/Server/src/MonoDebugProxy/ws-proxy/MonoProxy.cs +++ b/src/Components/Blazor/Server/src/MonoDebugProxy/ws-proxy/MonoProxy.cs @@ -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 (); int frame_id = 0; var the_mono_frames = res.Value? ["result"]? ["value"]? ["frames"]?.Values (); + foreach (var mono_frame in the_mono_frames) { var il_pos = mono_frame ["il_pos"].Value (); var method_token = mono_frame ["method_token"].Value (); var assembly_name = mono_frame ["assembly_name"].Value (); 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().ToArray(); + try { + var values = res.Value?["result"]?["value"]?.Values().ToArray() ?? Array.Empty(); + var var_list = new List(); - var var_list = new List(); + // 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 ().ToArray (); + try { + var values = res.Value? ["result"]? ["value"]?.Values ().ToArray (); - var var_list = new List (); - 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 (); + 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 EnableBreakPoint (Breakpoint bp, CancellationToken token) diff --git a/src/Components/Blazor/Server/src/MonoDebugProxy/ws-proxy/WsProxy.cs b/src/Components/Blazor/Server/src/MonoDebugProxy/ws-proxy/WsProxy.cs index 17c72e9ce7..87ef23027e 100644 --- a/src/Components/Blazor/Server/src/MonoDebugProxy/ws-proxy/WsProxy.cs +++ b/src/Components/Blazor/Server/src/MonoDebugProxy/ws-proxy/WsProxy.cs @@ -97,6 +97,7 @@ namespace WsProxy { internal class WsProxy { TaskCompletionSource side_exception = new TaskCompletionSource (); + TaskCompletionSource client_initiated_close = new TaskCompletionSource (); List<(int, TaskCompletionSource)> pending_cmds = new List<(int, TaskCompletionSource)> (); 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 (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 (), res ["method"].Value (), res ["params"] as JObject, token)); + if (!string.IsNullOrEmpty (msg)) { + var res = JObject.Parse (msg); + pending_ops.Add (OnCommand (res ["id"].Value (), res ["method"].Value (), res ["params"] as JObject, token)); + } } internal async Task 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)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)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)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)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 (); } } } diff --git a/src/Components/Blazor/Templates/src/Directory.Build.props b/src/Components/Blazor/Templates/src/Directory.Build.props deleted file mode 100644 index ed5e015014..0000000000 --- a/src/Components/Blazor/Templates/src/Directory.Build.props +++ /dev/null @@ -1,15 +0,0 @@ - - - - - false - false - - - - - 0.8.0-preview-19064-0339 - 3.0.0-preview-19064-0339 - - - diff --git a/src/Components/Blazor/Templates/src/Directory.Build.targets b/src/Components/Blazor/Templates/src/Directory.Build.targets deleted file mode 100644 index 7c6f423add..0000000000 --- a/src/Components/Blazor/Templates/src/Directory.Build.targets +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - TemplateBlazorVersion=$(PackageVersion); - TemplateComponentsVersion=$(ComponentsPackageVersion); - RepositoryCommit=$(SourceRevisionId); - - - - diff --git a/src/Components/Blazor/Templates/src/Microsoft.AspNetCore.Blazor.Templates.nuspec b/src/Components/Blazor/Templates/src/Microsoft.AspNetCore.Blazor.Templates.nuspec deleted file mode 100644 index fd19750231..0000000000 --- a/src/Components/Blazor/Templates/src/Microsoft.AspNetCore.Blazor.Templates.nuspec +++ /dev/null @@ -1,16 +0,0 @@ - - - - $CommonMetadataElements$ - - - - - - $CommonFileElements$ - - - diff --git a/src/Components/Blazor/Templates/src/content/BlazorWasm-CSharp/Client/Program.cs b/src/Components/Blazor/Templates/src/content/BlazorWasm-CSharp/Client/Program.cs deleted file mode 100644 index 03d510452d..0000000000 --- a/src/Components/Blazor/Templates/src/content/BlazorWasm-CSharp/Client/Program.cs +++ /dev/null @@ -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(); - } -} diff --git a/src/Components/Blazor/Templates/src/content/BlazorWasm-CSharp/Client/Startup.cs b/src/Components/Blazor/Templates/src/content/BlazorWasm-CSharp/Client/Startup.cs deleted file mode 100644 index b30f14ae06..0000000000 --- a/src/Components/Blazor/Templates/src/content/BlazorWasm-CSharp/Client/Startup.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Microsoft.AspNetCore.Components.Builder; -using Microsoft.Extensions.DependencyInjection; - -#if (Hosted) -namespace BlazorWasm_CSharp.Client -#else -namespace BlazorWasm_CSharp -#endif -{ - public class Startup - { - public void ConfigureServices(IServiceCollection services) - { - } - - public void Configure(IComponentsApplicationBuilder app) - { - app.AddComponent("app"); - } - } -} diff --git a/src/Components/Blazor/Templates/src/content/BlazorWasm-CSharp/Shared/BlazorWasm-CSharp.Shared.csproj b/src/Components/Blazor/Templates/src/content/BlazorWasm-CSharp/Shared/BlazorWasm-CSharp.Shared.csproj deleted file mode 100644 index 2a77f0c7cc..0000000000 --- a/src/Components/Blazor/Templates/src/content/BlazorWasm-CSharp/Shared/BlazorWasm-CSharp.Shared.csproj +++ /dev/null @@ -1,8 +0,0 @@ - - - - netstandard2.0 - 7.3 - - - diff --git a/src/Components/Blazor/Validation/src/Microsoft.AspNetCore.Blazor.DataAnnotations.Validation.csproj b/src/Components/Blazor/Validation/src/Microsoft.AspNetCore.Blazor.DataAnnotations.Validation.csproj index 89023db6b9..53cc678edb 100644 --- a/src/Components/Blazor/Validation/src/Microsoft.AspNetCore.Blazor.DataAnnotations.Validation.csproj +++ b/src/Components/Blazor/Validation/src/Microsoft.AspNetCore.Blazor.DataAnnotations.Validation.csproj @@ -1,7 +1,7 @@ - netstandard2.0 + netstandard2.1 Provides experimental support for validation using DataAnnotations. false false diff --git a/src/Components/Blazor/testassets/HostedInAspNet.Client/HostedInAspNet.Client.csproj b/src/Components/Blazor/testassets/HostedInAspNet.Client/HostedInAspNet.Client.csproj index ef12ac3c4e..e27de695c1 100644 --- a/src/Components/Blazor/testassets/HostedInAspNet.Client/HostedInAspNet.Client.csproj +++ b/src/Components/Blazor/testassets/HostedInAspNet.Client/HostedInAspNet.Client.csproj @@ -1,7 +1,7 @@  - netstandard2.0 + netstandard2.1 Exe true 3.0 @@ -11,17 +11,4 @@ - - - $(GetCurrentProjectStaticWebAssetsDependsOn); - _ClearCurrentStaticWebAssetsForReferenceDiscovery - - - - - - - - - diff --git a/src/Components/Blazor/testassets/HostedInAspNet.Client/Program.cs b/src/Components/Blazor/testassets/HostedInAspNet.Client/Program.cs index 69ee439533..e922c2996f 100644 --- a/src/Components/Blazor/testassets/HostedInAspNet.Client/Program.cs +++ b/src/Components/Blazor/testassets/HostedInAspNet.Client/Program.cs @@ -1,19 +1,19 @@ // 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 Microsoft.AspNetCore.Blazor.Hosting; namespace HostedInAspNet.Client { public class Program { - public static void Main(string[] args) + public static async Task Main(string[] args) { - CreateHostBuilder(args).Build().Run(); - } + var builder = WebAssemblyHostBuilder.CreateDefault(args); + builder.RootComponents.Add("app"); - public static IWebAssemblyHostBuilder CreateHostBuilder(string[] args) => - BlazorWebAssemblyHost.CreateDefaultBuilder() - .UseBlazorStartup(); + await builder.Build().RunAsync(); + } } } diff --git a/src/Components/Blazor/testassets/HostedInAspNet.Client/Startup.cs b/src/Components/Blazor/testassets/HostedInAspNet.Client/Startup.cs deleted file mode 100644 index a06163b9e0..0000000000 --- a/src/Components/Blazor/testassets/HostedInAspNet.Client/Startup.cs +++ /dev/null @@ -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. - -using Microsoft.AspNetCore.Components.Builder; -using Microsoft.Extensions.DependencyInjection; - -namespace HostedInAspNet.Client -{ - public class Startup - { - public void ConfigureServices(IServiceCollection services) - { - } - - public void Configure(IComponentsApplicationBuilder app) - { - app.AddComponent("app"); - } - } -} diff --git a/src/Components/Blazor/testassets/HostedInAspNet.Server/HostedInAspNet.Server.csproj b/src/Components/Blazor/testassets/HostedInAspNet.Server/HostedInAspNet.Server.csproj index afc09e4f77..483ea4e8f4 100644 --- a/src/Components/Blazor/testassets/HostedInAspNet.Server/HostedInAspNet.Server.csproj +++ b/src/Components/Blazor/testassets/HostedInAspNet.Server/HostedInAspNet.Server.csproj @@ -2,6 +2,9 @@ $(DefaultNetCoreTargetFramework) + + true + @@ -10,8 +13,6 @@ - - diff --git a/src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/Program.cs b/src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/Program.cs deleted file mode 100644 index f498eb0222..0000000000 --- a/src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/Program.cs +++ /dev/null @@ -1,19 +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.Hosting; - -namespace Microsoft.AspNetCore.Blazor.E2EPerformance -{ - public class Program - { - public static void Main(string[] args) - { - CreateHostBuilder(args).Build().Run(); - } - - public static IWebAssemblyHostBuilder CreateHostBuilder(string[] args) => - BlazorWebAssemblyHost.CreateDefaultBuilder() - .UseBlazorStartup(); - } -} diff --git a/src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/Startup.cs b/src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/Startup.cs deleted file mode 100644 index 7422cd806c..0000000000 --- a/src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/Startup.cs +++ /dev/null @@ -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. - -using Microsoft.AspNetCore.Components.Builder; -using Microsoft.Extensions.DependencyInjection; - -namespace Microsoft.AspNetCore.Blazor.E2EPerformance -{ - public class Startup - { - public void ConfigureServices(IServiceCollection services) - { - } - - public void Configure(IComponentsApplicationBuilder app) - { - app.AddComponent("app"); - } - } -} diff --git a/src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/wwwroot/benchmarks/index.js b/src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/wwwroot/benchmarks/index.js deleted file mode 100644 index 4600066f38..0000000000 --- a/src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/wwwroot/benchmarks/index.js +++ /dev/null @@ -1,6 +0,0 @@ -import { HtmlUI } from './lib/minibench/minibench.js'; -import './appStartup.js'; -import './renderList.js'; -import './jsonHandling.js'; - -new HtmlUI('E2E Performance', '#display'); diff --git a/src/Components/Blazor/testassets/MonoSanity/MonoSanity.csproj b/src/Components/Blazor/testassets/MonoSanity/MonoSanity.csproj index 5297f5bca6..464f63b57c 100644 --- a/src/Components/Blazor/testassets/MonoSanity/MonoSanity.csproj +++ b/src/Components/Blazor/testassets/MonoSanity/MonoSanity.csproj @@ -2,6 +2,8 @@ $(DefaultNetCoreTargetFramework) + + true @@ -9,9 +11,7 @@ - - diff --git a/src/Components/Blazor/testassets/MonoSanity/wwwroot/index.html b/src/Components/Blazor/testassets/MonoSanity/wwwroot/index.html index c4d1ab60e2..8a42e8e5d1 100644 --- a/src/Components/Blazor/testassets/MonoSanity/wwwroot/index.html +++ b/src/Components/Blazor/testassets/MonoSanity/wwwroot/index.html @@ -36,14 +36,6 @@ -
- Invoke wiped method -
- -
-
-
-
Call JS from .NET
@@ -111,16 +103,6 @@ } }; - el('invokeWipedMethod').onsubmit = function (evt) { - evt.preventDefault(); - try { - invokeMonoMethod('MonoSanityClient', 'MonoSanityClient', 'Examples', 'InvokeWipedMethod', []); - el('invokeWipedMethodStackTrace').value = 'WARNING: No exception occurred'; - } catch (ex) { - el('invokeWipedMethodStackTrace').value = ex.toString(); - } - }; - el('callJs').onsubmit = function (evt) { evt.preventDefault(); var expression = el('callJsEvalExpression').value; diff --git a/src/Components/Blazor/testassets/MonoSanity/wwwroot/loader.js b/src/Components/Blazor/testassets/MonoSanity/wwwroot/loader.js index 48d4530d3e..328acacdff 100644 --- a/src/Components/Blazor/testassets/MonoSanity/wwwroot/loader.js +++ b/src/Components/Blazor/testassets/MonoSanity/wwwroot/loader.js @@ -12,7 +12,7 @@ window.initMono = function initMono(loadAssemblyUrls, onReadyCallback) { window.Module = { locateFile: function (fileName) { - return fileName === 'mono.wasm' ? '/_framework/wasm/mono.wasm' : fileName; + return fileName === 'dotnet.wasm' ? '/_framework/wasm/dotnet.wasm' : fileName; }, onRuntimeInitialized: function () { var allAssemblyUrls = loadAssemblyUrls.concat([ @@ -117,7 +117,7 @@ } var scriptElem = document.createElement('script'); - scriptElem.src = '/_framework/wasm/mono.js'; + scriptElem.src = '/_framework/wasm/dotnet.js'; document.body.appendChild(scriptElem); } diff --git a/src/Components/Blazor/testassets/MonoSanityClient/Examples.cs b/src/Components/Blazor/testassets/MonoSanityClient/Examples.cs index 8023ded4d9..1d56128e35 100644 --- a/src/Components/Blazor/testassets/MonoSanityClient/Examples.cs +++ b/src/Components/Blazor/testassets/MonoSanityClient/Examples.cs @@ -31,11 +31,6 @@ namespace MonoSanityClient throw new InvalidOperationException(message); } - public static void InvokeWipedMethod() - { - new HttpClientHandler().Dispose(); - } - public static string EvaluateJavaScript(string expression) { var result = InternalCalls.InvokeJSUnmarshalled(out var exceptionMessage, "evaluateJsExpression", expression, null, null); diff --git a/src/Components/Blazor/testassets/MonoSanityClient/MonoSanityClient.csproj b/src/Components/Blazor/testassets/MonoSanityClient/MonoSanityClient.csproj index b186c39194..e01c600843 100644 --- a/src/Components/Blazor/testassets/MonoSanityClient/MonoSanityClient.csproj +++ b/src/Components/Blazor/testassets/MonoSanityClient/MonoSanityClient.csproj @@ -1,7 +1,7 @@  - netstandard2.0 + netstandard2.1 false false exe diff --git a/src/Components/Blazor/testassets/StandaloneApp/Program.cs b/src/Components/Blazor/testassets/StandaloneApp/Program.cs index 530de72870..8da14834b6 100644 --- a/src/Components/Blazor/testassets/StandaloneApp/Program.cs +++ b/src/Components/Blazor/testassets/StandaloneApp/Program.cs @@ -1,19 +1,19 @@ // 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 Microsoft.AspNetCore.Blazor.Hosting; namespace StandaloneApp { public class Program { - public static void Main(string[] args) + public static async Task Main(string[] args) { - CreateHostBuilder(args).Build().Run(); - } + var builder = WebAssemblyHostBuilder.CreateDefault(args); + builder.RootComponents.Add("app"); - public static IWebAssemblyHostBuilder CreateHostBuilder(string[] args) => - BlazorWebAssemblyHost.CreateDefaultBuilder() - .UseBlazorStartup(); + await builder.Build().RunAsync(); + } } } diff --git a/src/Components/Blazor/testassets/StandaloneApp/StandaloneApp.csproj b/src/Components/Blazor/testassets/StandaloneApp/StandaloneApp.csproj index cddd429b6a..32156c56b8 100644 --- a/src/Components/Blazor/testassets/StandaloneApp/StandaloneApp.csproj +++ b/src/Components/Blazor/testassets/StandaloneApp/StandaloneApp.csproj @@ -1,7 +1,7 @@  - netstandard2.0 + netstandard2.1 true 3.0 diff --git a/src/Components/Blazor/testassets/StandaloneApp/Startup.cs b/src/Components/Blazor/testassets/StandaloneApp/Startup.cs deleted file mode 100644 index 79564e8d9d..0000000000 --- a/src/Components/Blazor/testassets/StandaloneApp/Startup.cs +++ /dev/null @@ -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. - -using Microsoft.AspNetCore.Components.Builder; -using Microsoft.Extensions.DependencyInjection; - -namespace StandaloneApp -{ - public class Startup - { - public void ConfigureServices(IServiceCollection services) - { - } - - public void Configure(IComponentsApplicationBuilder app) - { - app.AddComponent("app"); - } - } -} diff --git a/src/Components/Blazor/testassets/StandaloneApp/wwwroot/index.html b/src/Components/Blazor/testassets/StandaloneApp/wwwroot/index.html index 5da6ba26b3..646ea2f3da 100644 --- a/src/Components/Blazor/testassets/StandaloneApp/wwwroot/index.html +++ b/src/Components/Blazor/testassets/StandaloneApp/wwwroot/index.html @@ -1,8 +1,9 @@ - + - + + Blazor standalone @@ -11,6 +12,12 @@ Loading... +
+ An unhandled exception has occurred. See browser dev tools for details. + Reload + 🗙 +
+ diff --git a/src/Components/Components.sln b/src/Components/Components.sln index ba0b2476ff..c88695cf66 100644 --- a/src/Components/Components.sln +++ b/src/Components/Components.sln @@ -21,12 +21,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Blazor EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Blazor.DevServer", "Blazor\DevServer\src\Microsoft.AspNetCore.Blazor.DevServer.csproj", "{A6C8050D-7C18-4585-ADCF-833AC1765847}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Blazor.E2EPerformance", "Blazor\testassets\Microsoft.AspNetCore.Blazor.E2EPerformance\Microsoft.AspNetCore.Blazor.E2EPerformance.csproj", "{08773DD6-6FED-4BF2-BD9F-C19D2CF919BB}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Blazor.Server", "Blazor\Server\src\Microsoft.AspNetCore.Blazor.Server.csproj", "{A4859630-F9F7-4F5C-9FF3-6C013D7C58FA}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Blazor.Templates", "Blazor\Templates\src\Microsoft.AspNetCore.Blazor.Templates.csproj", "{66036B70-6C93-4E45-A1A1-819F15CA757A}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "testassets", "testassets", "{A7ABAC29-F73F-456D-AE54-46842CFC2E10}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HostedInAspNet.Client", "Blazor\testassets\HostedInAspNet.Client\HostedInAspNet.Client.csproj", "{FD37F740-A654-4117-BFB6-9112CE4C1D3B}" @@ -240,12 +236,24 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ignitor", "Ignitor\src\Igni EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ignitor.Test", "Ignitor\test\Ignitor.Test.csproj", "{F31E8118-014E-4CCE-8A48-5282F7B9BB3E}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Validation", "Validation", "{FD9BD646-9D50-42ED-A3E1-90558BA0C6B2}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Blazor.DataAnnotations.Validation", "Blazor\Validation\src\Microsoft.AspNetCore.Blazor.DataAnnotations.Validation.csproj", "{B70F90C7-2696-4050-B24E-BF0308F4E059}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Blazor.DataAnnotations.Validation.Tests", "Blazor\Validation\test\Microsoft.AspNetCore.Blazor.DataAnnotations.Validation.Tests.csproj", "{A5617A9D-C71E-44DE-936C-27611EB40A02}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Mono.WebAssembly.Interop", "Mono.WebAssembly.Interop", "{21BB9C13-20C1-4F2B-80E4-D7C64AA3BD05}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mono.WebAssembly.Interop", "Blazor\Mono.WebAssembly.Interop\src\Mono.WebAssembly.Interop.csproj", "{D141CFEE-D10A-406B-8963-F86FA13732E3}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ComponentsApp.Server", "test\testassets\ComponentsApp.Server\ComponentsApp.Server.csproj", "{F2E27E1C-2E47-42C1-9AC7-36265A381717}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "benchmarkapps", "benchmarkapps", "{CCC82E97-7B58-43E2-BBBD-23D82F926367}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Wasm.Performance", "Wasm.Performance", "{F65EFF0F-ACF3-46BD-9A8F-CDA94AF1885A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wasm.Performance.Driver", "benchmarkapps\Wasm.Performance\Driver\Wasm.Performance.Driver.csproj", "{CA9948CA-B3FA-4C2E-A726-5E47BAD19457}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wasm.Performance.TestApp", "benchmarkapps\Wasm.Performance\TestApp\Wasm.Performance.TestApp.csproj", "{97EA0A7D-FE5E-47D1-ADDC-4BFD702F55AB}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -340,18 +348,6 @@ Global {A6C8050D-7C18-4585-ADCF-833AC1765847}.Release|x64.Build.0 = Release|Any CPU {A6C8050D-7C18-4585-ADCF-833AC1765847}.Release|x86.ActiveCfg = Release|Any CPU {A6C8050D-7C18-4585-ADCF-833AC1765847}.Release|x86.Build.0 = Release|Any CPU - {08773DD6-6FED-4BF2-BD9F-C19D2CF919BB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {08773DD6-6FED-4BF2-BD9F-C19D2CF919BB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {08773DD6-6FED-4BF2-BD9F-C19D2CF919BB}.Debug|x64.ActiveCfg = Debug|Any CPU - {08773DD6-6FED-4BF2-BD9F-C19D2CF919BB}.Debug|x64.Build.0 = Debug|Any CPU - {08773DD6-6FED-4BF2-BD9F-C19D2CF919BB}.Debug|x86.ActiveCfg = Debug|Any CPU - {08773DD6-6FED-4BF2-BD9F-C19D2CF919BB}.Debug|x86.Build.0 = Debug|Any CPU - {08773DD6-6FED-4BF2-BD9F-C19D2CF919BB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {08773DD6-6FED-4BF2-BD9F-C19D2CF919BB}.Release|Any CPU.Build.0 = Release|Any CPU - {08773DD6-6FED-4BF2-BD9F-C19D2CF919BB}.Release|x64.ActiveCfg = Release|Any CPU - {08773DD6-6FED-4BF2-BD9F-C19D2CF919BB}.Release|x64.Build.0 = Release|Any CPU - {08773DD6-6FED-4BF2-BD9F-C19D2CF919BB}.Release|x86.ActiveCfg = Release|Any CPU - {08773DD6-6FED-4BF2-BD9F-C19D2CF919BB}.Release|x86.Build.0 = Release|Any CPU {A4859630-F9F7-4F5C-9FF3-6C013D7C58FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A4859630-F9F7-4F5C-9FF3-6C013D7C58FA}.Debug|Any CPU.Build.0 = Debug|Any CPU {A4859630-F9F7-4F5C-9FF3-6C013D7C58FA}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -364,18 +360,6 @@ Global {A4859630-F9F7-4F5C-9FF3-6C013D7C58FA}.Release|x64.Build.0 = Release|Any CPU {A4859630-F9F7-4F5C-9FF3-6C013D7C58FA}.Release|x86.ActiveCfg = Release|Any CPU {A4859630-F9F7-4F5C-9FF3-6C013D7C58FA}.Release|x86.Build.0 = Release|Any CPU - {66036B70-6C93-4E45-A1A1-819F15CA757A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {66036B70-6C93-4E45-A1A1-819F15CA757A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {66036B70-6C93-4E45-A1A1-819F15CA757A}.Debug|x64.ActiveCfg = Debug|Any CPU - {66036B70-6C93-4E45-A1A1-819F15CA757A}.Debug|x64.Build.0 = Debug|Any CPU - {66036B70-6C93-4E45-A1A1-819F15CA757A}.Debug|x86.ActiveCfg = Debug|Any CPU - {66036B70-6C93-4E45-A1A1-819F15CA757A}.Debug|x86.Build.0 = Debug|Any CPU - {66036B70-6C93-4E45-A1A1-819F15CA757A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {66036B70-6C93-4E45-A1A1-819F15CA757A}.Release|Any CPU.Build.0 = Release|Any CPU - {66036B70-6C93-4E45-A1A1-819F15CA757A}.Release|x64.ActiveCfg = Release|Any CPU - {66036B70-6C93-4E45-A1A1-819F15CA757A}.Release|x64.Build.0 = Release|Any CPU - {66036B70-6C93-4E45-A1A1-819F15CA757A}.Release|x86.ActiveCfg = Release|Any CPU - {66036B70-6C93-4E45-A1A1-819F15CA757A}.Release|x86.Build.0 = Release|Any CPU {FD37F740-A654-4117-BFB6-9112CE4C1D3B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {FD37F740-A654-4117-BFB6-9112CE4C1D3B}.Debug|Any CPU.Build.0 = Debug|Any CPU {FD37F740-A654-4117-BFB6-9112CE4C1D3B}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -1516,6 +1500,54 @@ Global {A5617A9D-C71E-44DE-936C-27611EB40A02}.Release|x64.Build.0 = Release|Any CPU {A5617A9D-C71E-44DE-936C-27611EB40A02}.Release|x86.ActiveCfg = Release|Any CPU {A5617A9D-C71E-44DE-936C-27611EB40A02}.Release|x86.Build.0 = Release|Any CPU + {D141CFEE-D10A-406B-8963-F86FA13732E3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D141CFEE-D10A-406B-8963-F86FA13732E3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D141CFEE-D10A-406B-8963-F86FA13732E3}.Debug|x64.ActiveCfg = Debug|Any CPU + {D141CFEE-D10A-406B-8963-F86FA13732E3}.Debug|x64.Build.0 = Debug|Any CPU + {D141CFEE-D10A-406B-8963-F86FA13732E3}.Debug|x86.ActiveCfg = Debug|Any CPU + {D141CFEE-D10A-406B-8963-F86FA13732E3}.Debug|x86.Build.0 = Debug|Any CPU + {D141CFEE-D10A-406B-8963-F86FA13732E3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D141CFEE-D10A-406B-8963-F86FA13732E3}.Release|Any CPU.Build.0 = Release|Any CPU + {D141CFEE-D10A-406B-8963-F86FA13732E3}.Release|x64.ActiveCfg = Release|Any CPU + {D141CFEE-D10A-406B-8963-F86FA13732E3}.Release|x64.Build.0 = Release|Any CPU + {D141CFEE-D10A-406B-8963-F86FA13732E3}.Release|x86.ActiveCfg = Release|Any CPU + {D141CFEE-D10A-406B-8963-F86FA13732E3}.Release|x86.Build.0 = Release|Any CPU + {F2E27E1C-2E47-42C1-9AC7-36265A381717}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F2E27E1C-2E47-42C1-9AC7-36265A381717}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F2E27E1C-2E47-42C1-9AC7-36265A381717}.Debug|x64.ActiveCfg = Debug|Any CPU + {F2E27E1C-2E47-42C1-9AC7-36265A381717}.Debug|x64.Build.0 = Debug|Any CPU + {F2E27E1C-2E47-42C1-9AC7-36265A381717}.Debug|x86.ActiveCfg = Debug|Any CPU + {F2E27E1C-2E47-42C1-9AC7-36265A381717}.Debug|x86.Build.0 = Debug|Any CPU + {F2E27E1C-2E47-42C1-9AC7-36265A381717}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F2E27E1C-2E47-42C1-9AC7-36265A381717}.Release|Any CPU.Build.0 = Release|Any CPU + {F2E27E1C-2E47-42C1-9AC7-36265A381717}.Release|x64.ActiveCfg = Release|Any CPU + {F2E27E1C-2E47-42C1-9AC7-36265A381717}.Release|x64.Build.0 = Release|Any CPU + {F2E27E1C-2E47-42C1-9AC7-36265A381717}.Release|x86.ActiveCfg = Release|Any CPU + {F2E27E1C-2E47-42C1-9AC7-36265A381717}.Release|x86.Build.0 = Release|Any CPU + {CA9948CA-B3FA-4C2E-A726-5E47BAD19457}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CA9948CA-B3FA-4C2E-A726-5E47BAD19457}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CA9948CA-B3FA-4C2E-A726-5E47BAD19457}.Debug|x64.ActiveCfg = Debug|Any CPU + {CA9948CA-B3FA-4C2E-A726-5E47BAD19457}.Debug|x64.Build.0 = Debug|Any CPU + {CA9948CA-B3FA-4C2E-A726-5E47BAD19457}.Debug|x86.ActiveCfg = Debug|Any CPU + {CA9948CA-B3FA-4C2E-A726-5E47BAD19457}.Debug|x86.Build.0 = Debug|Any CPU + {CA9948CA-B3FA-4C2E-A726-5E47BAD19457}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CA9948CA-B3FA-4C2E-A726-5E47BAD19457}.Release|Any CPU.Build.0 = Release|Any CPU + {CA9948CA-B3FA-4C2E-A726-5E47BAD19457}.Release|x64.ActiveCfg = Release|Any CPU + {CA9948CA-B3FA-4C2E-A726-5E47BAD19457}.Release|x64.Build.0 = Release|Any CPU + {CA9948CA-B3FA-4C2E-A726-5E47BAD19457}.Release|x86.ActiveCfg = Release|Any CPU + {CA9948CA-B3FA-4C2E-A726-5E47BAD19457}.Release|x86.Build.0 = Release|Any CPU + {97EA0A7D-FE5E-47D1-ADDC-4BFD702F55AB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {97EA0A7D-FE5E-47D1-ADDC-4BFD702F55AB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {97EA0A7D-FE5E-47D1-ADDC-4BFD702F55AB}.Debug|x64.ActiveCfg = Debug|Any CPU + {97EA0A7D-FE5E-47D1-ADDC-4BFD702F55AB}.Debug|x64.Build.0 = Debug|Any CPU + {97EA0A7D-FE5E-47D1-ADDC-4BFD702F55AB}.Debug|x86.ActiveCfg = Debug|Any CPU + {97EA0A7D-FE5E-47D1-ADDC-4BFD702F55AB}.Debug|x86.Build.0 = Debug|Any CPU + {97EA0A7D-FE5E-47D1-ADDC-4BFD702F55AB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {97EA0A7D-FE5E-47D1-ADDC-4BFD702F55AB}.Release|Any CPU.Build.0 = Release|Any CPU + {97EA0A7D-FE5E-47D1-ADDC-4BFD702F55AB}.Release|x64.ActiveCfg = Release|Any CPU + {97EA0A7D-FE5E-47D1-ADDC-4BFD702F55AB}.Release|x64.Build.0 = Release|Any CPU + {97EA0A7D-FE5E-47D1-ADDC-4BFD702F55AB}.Release|x86.ActiveCfg = Release|Any CPU + {97EA0A7D-FE5E-47D1-ADDC-4BFD702F55AB}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1528,9 +1560,7 @@ Global {E8AD67A4-77D3-4B85-AE19-4711388B62B1} = {7260DED9-22A9-4E9D-92F4-5E8A4404DEAF} {E38FDBB0-08C1-444E-A449-69C8A59D721B} = {7260DED9-22A9-4E9D-92F4-5E8A4404DEAF} {A6C8050D-7C18-4585-ADCF-833AC1765847} = {7260DED9-22A9-4E9D-92F4-5E8A4404DEAF} - {08773DD6-6FED-4BF2-BD9F-C19D2CF919BB} = {7260DED9-22A9-4E9D-92F4-5E8A4404DEAF} {A4859630-F9F7-4F5C-9FF3-6C013D7C58FA} = {7260DED9-22A9-4E9D-92F4-5E8A4404DEAF} - {66036B70-6C93-4E45-A1A1-819F15CA757A} = {7260DED9-22A9-4E9D-92F4-5E8A4404DEAF} {A7ABAC29-F73F-456D-AE54-46842CFC2E10} = {7260DED9-22A9-4E9D-92F4-5E8A4404DEAF} {FD37F740-A654-4117-BFB6-9112CE4C1D3B} = {A7ABAC29-F73F-456D-AE54-46842CFC2E10} {C1E2C117-BE47-4E29-94B3-753262D97A5C} = {A7ABAC29-F73F-456D-AE54-46842CFC2E10} @@ -1626,9 +1656,14 @@ Global {BBF37AF9-8290-4B70-8BA8-0F6017B3B620} = {46E4300C-5726-4108-B9A2-18BB94EB26ED} {CD0EF85C-4187-4515-A355-E5A0D4485F40} = {BDE2397D-C53A-4783-8B3A-1F54F48A6926} {F31E8118-014E-4CCE-8A48-5282F7B9BB3E} = {BDE2397D-C53A-4783-8B3A-1F54F48A6926} - {FD9BD646-9D50-42ED-A3E1-90558BA0C6B2} = {7260DED9-22A9-4E9D-92F4-5E8A4404DEAF} {B70F90C7-2696-4050-B24E-BF0308F4E059} = {7260DED9-22A9-4E9D-92F4-5E8A4404DEAF} {A5617A9D-C71E-44DE-936C-27611EB40A02} = {7260DED9-22A9-4E9D-92F4-5E8A4404DEAF} + {21BB9C13-20C1-4F2B-80E4-D7C64AA3BD05} = {7260DED9-22A9-4E9D-92F4-5E8A4404DEAF} + {D141CFEE-D10A-406B-8963-F86FA13732E3} = {21BB9C13-20C1-4F2B-80E4-D7C64AA3BD05} + {F2E27E1C-2E47-42C1-9AC7-36265A381717} = {44E0D4F3-4430-4175-B482-0D1AEE4BB699} + {F65EFF0F-ACF3-46BD-9A8F-CDA94AF1885A} = {CCC82E97-7B58-43E2-BBBD-23D82F926367} + {CA9948CA-B3FA-4C2E-A726-5E47BAD19457} = {F65EFF0F-ACF3-46BD-9A8F-CDA94AF1885A} + {97EA0A7D-FE5E-47D1-ADDC-4BFD702F55AB} = {F65EFF0F-ACF3-46BD-9A8F-CDA94AF1885A} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {CC3C47E1-AD1A-4619-9CD3-E08A0148E5CE} diff --git a/src/Components/ComponentsNoDeps.slnf b/src/Components/ComponentsNoDeps.slnf index 09f6a0859f..7e09eeea25 100644 --- a/src/Components/ComponentsNoDeps.slnf +++ b/src/Components/ComponentsNoDeps.slnf @@ -13,13 +13,12 @@ "Blazor\\DevServer\\src\\Microsoft.AspNetCore.Blazor.DevServer.csproj", "Blazor\\Http\\src\\Microsoft.AspNetCore.Blazor.HttpClient.csproj", "Blazor\\Http\\test\\Microsoft.AspNetCore.Blazor.HttpClient.Tests.csproj", + "Blazor\\Mono.WebAssembly.Interop\\src\\Mono.WebAssembly.Interop.csproj", "Blazor\\Server\\src\\Microsoft.AspNetCore.Blazor.Server.csproj", - "Blazor\\Templates\\src\\Microsoft.AspNetCore.Blazor.Templates.csproj", "Blazor\\Validation\\src\\Microsoft.AspNetCore.Blazor.DataAnnotations.Validation.csproj", "Blazor\\Validation\\test\\Microsoft.AspNetCore.Blazor.DataAnnotations.Validation.Tests.csproj", "Blazor\\testassets\\HostedInAspNet.Client\\HostedInAspNet.Client.csproj", "Blazor\\testassets\\HostedInAspNet.Server\\HostedInAspNet.Server.csproj", - "Blazor\\testassets\\Microsoft.AspNetCore.Blazor.E2EPerformance\\Microsoft.AspNetCore.Blazor.E2EPerformance.csproj", "Blazor\\testassets\\MonoSanityClient\\MonoSanityClient.csproj", "Blazor\\testassets\\MonoSanity\\MonoSanity.csproj", "Blazor\\testassets\\StandaloneApp\\StandaloneApp.csproj", @@ -35,6 +34,8 @@ "Server\\test\\Microsoft.AspNetCore.Components.Server.Tests.csproj", "Web\\src\\Microsoft.AspNetCore.Components.Web.csproj", "Web\\test\\Microsoft.AspNetCore.Components.Web.Tests.csproj", + "benchmarkapps\\Wasm.Performance\\Driver\\Wasm.Performance.Driver.csproj", + "benchmarkapps\\Wasm.Performance\\TestApp\\Wasm.Performance.TestApp.csproj", "test\\E2ETest\\Microsoft.AspNetCore.Components.E2ETests.csproj", "test\\testassets\\BasicTestApp\\BasicTestApp.csproj", "test\\testassets\\TestContentPackage\\TestContentPackage.csproj", diff --git a/src/Components/Directory.Build.props b/src/Components/Directory.Build.props index b017bb81ea..fb709cba97 100644 --- a/src/Components/Directory.Build.props +++ b/src/Components/Directory.Build.props @@ -12,6 +12,10 @@ aspnetcore;components + + 3.1.0 + $(MSBuildThisFileDirectory)Shared\ diff --git a/src/Components/Directory.Build.targets b/src/Components/Directory.Build.targets index a3eb973629..d6569c4088 100644 --- a/src/Components/Directory.Build.targets +++ b/src/Components/Directory.Build.targets @@ -3,6 +3,26 @@ true + + + + + + netcoreapp3.1 + Microsoft.AspNetCore.App + $(LatestAspNetCoreReferenceVersion) + $(LatestAspNetCoreReferenceVersion) + Microsoft.AspNetCore.App.Ref + $(LatestAspNetCoreReferenceVersion) + Microsoft.AspNetCore.App.Runtime.**RID** + linux-arm;linux-arm64;linux-musl-arm64;linux-musl-x64;linux-x64;osx-x64;rhel.6-x64;tizen.4.0.0-armel;tizen.5.0.0-armel;win-arm;win-arm64;win-x64;win-x86 + true + + + + diff --git a/src/Components/Web.JS/dist/Release/blazor.webassembly.js b/src/Components/Web.JS/dist/Release/blazor.webassembly.js index 5279765b23..16dd578625 100644 --- a/src/Components/Web.JS/dist/Release/blazor.webassembly.js +++ b/src/Components/Web.JS/dist/Release/blazor.webassembly.js @@ -1 +1 @@ -!function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=45)}([,,,,,function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),n(25),n(18);var r=n(26),o=n(13),a={},i=!1;function l(e,t,n){var o=a[e];o||(o=a[e]=new r.BrowserRenderer(e)),o.attachRootComponentToLogicalElement(n,t)}t.attachRootComponentToLogicalElement=l,t.attachRootComponentToElement=function(e,t,n){var r=document.querySelector(e);if(!r)throw new Error("Could not find any element matching selector '"+e+"'.");l(n||0,o.toLogicalElement(r,!0),t)},t.renderBatch=function(e,t){var n=a[e];if(!n)throw new Error("There is no browser renderer with ID "+e+".");for(var r=t.arrayRangeReader,o=t.updatedComponents(),l=r.values(o),u=r.count(o),s=t.referenceFrames(),c=r.values(s),d=t.diffReader,f=0;f0&&o[o.length-1])&&(6===a[0]||2===a[0])){i=0;continue}if(3===a[0]&&(!o||a[1]>o[0]&&a[1]0&&!t)throw new Error("New logical elements must start empty, or allowExistingContents must be true");return e[r]=[],e}function l(e,t,n){var a=e;if(e instanceof Comment&&(s(a)&&s(a).length>0))throw new Error("Not implemented: inserting non-empty logical container");if(u(a))throw new Error("Not implemented: moving existing logical children");var i=s(t);if(n0;)e(r,0);var a=r;a.parentNode.removeChild(a)},t.getLogicalParent=u,t.getLogicalSiblingEnd=function(e){return e[a]||null},t.getLogicalChild=function(e,t){return s(e)[t]},t.isSvgElement=function(e){return"http://www.w3.org/2000/svg"===c(e).namespaceURI},t.getLogicalChildrenArray=s,t.permuteLogicalChildren=function(e,t){var n=s(e);t.forEach(function(e){e.moveRangeStart=n[e.fromSiblingIndex],e.moveRangeEnd=function e(t){if(t instanceof Element)return t;var n=d(t);if(n)return n.previousSibling;var r=u(t);return r instanceof Element?r.lastChild:e(r)}(e.moveRangeStart)}),t.forEach(function(t){var r=t.moveToBeforeMarker=document.createComment("marker"),o=n[t.toSiblingIndex+1];o?o.parentNode.insertBefore(r,o):f(r,e)}),t.forEach(function(e){for(var t=e.moveToBeforeMarker,n=t.parentNode,r=e.moveRangeStart,o=e.moveRangeEnd,a=r;a;){var i=a.nextSibling;if(n.insertBefore(a,t),a===o)break;a=i}n.removeChild(t)}),t.forEach(function(e){n[e.toSiblingIndex]=e.moveRangeStart})},t.getClosestDomElement=c},,,,function(e,t,n){"use strict";var r;!function(e){window.DotNet=e;var t=[],n={},r={},o=1,a=null;function i(e){t.push(e)}function l(e,t,n,r){var o=s();if(o.invokeDotNetFromJS){var a=JSON.stringify(r,h),i=o.invokeDotNetFromJS(e,t,n,a);return i?d(i):null}throw new Error("The current dispatcher does not support synchronous calls from JS to .NET. Use invokeMethodAsync instead.")}function u(e,t,r,a){if(e&&r)throw new Error("For instance method calls, assemblyName should be null. Received '"+e+"'.");var i=o++,l=new Promise(function(e,t){n[i]={resolve:e,reject:t}});try{var u=JSON.stringify(a,h);s().beginInvokeDotNetFromJS(i,e,t,r,u)}catch(e){c(i,!1,e)}return l}function s(){if(null!==a)return a;throw new Error("No .NET call dispatcher has been set.")}function c(e,t,r){if(!n.hasOwnProperty(e))throw new Error("There is no pending async call with ID "+e+".");var o=n[e];delete n[e],t?o.resolve(r):o.reject(r)}function d(e){return e?JSON.parse(e,function(e,n){return t.reduce(function(t,n){return n(e,t)},n)}):null}function f(e){return e instanceof Error?e.message+"\n"+e.stack:e?e.toString():"null"}function p(e){if(r.hasOwnProperty(e))return r[e];var t,n=window,o="window";if(e.split(".").forEach(function(e){if(!(e in n))throw new Error("Could not find '"+e+"' in '"+o+"'.");t=n,n=n[e],o+="."+e}),n instanceof Function)return n=n.bind(t),r[e]=n,n;throw new Error("The value '"+o+"' is not a function.")}e.attachDispatcher=function(e){a=e},e.attachReviver=i,e.invokeMethod=function(e,t){for(var n=[],r=2;r0&&o[o.length-1])&&(6===a[0]||2===a[0])){i=0;continue}if(3===a[0]&&(!o||a[1]>o[0]&&a[1]0&&o[o.length-1])&&(6===a[0]||2===a[0])){i=0;continue}if(3===a[0]&&(!o||a[1]>o[0]&&a[1]-1?a.substring(0,l):"",s=l>-1?a.substring(l+1):a,c=t.monoPlatform.findMethod(e,u,s,i);t.monoPlatform.callMethod(c,null,r)},callMethod:function(e,n,r){if(r.length>4)throw new Error("Currently, MonoPlatform supports passing a maximum of 4 arguments from JS to .NET. You tried to pass "+r.length+".");var o=Module.stackSave();try{for(var a=Module.stackAlloc(r.length),l=Module.stackAlloc(4),u=0;u>2,r=Module.HEAPU32[n+1];if(r>y)throw new Error("Cannot read uint64 with high order part "+r+", because the result would exceed Number.MAX_SAFE_INTEGER.");return r*v+Module.HEAPU32[n]},readFloatField:function(e,t){return Module.getValue(e+(t||0),"float")},readObjectField:function(e,t){return Module.getValue(e+(t||0),"i32")},readStringField:function(e,n){var r=Module.getValue(e+(n||0),"i32");return 0===r?null:t.monoPlatform.toJavaScriptString(r)},readStructField:function(e,t){return e+(t||0)}};var w=document.createElement("a");function E(e){return e+12}},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=n(32),o=window.chrome&&navigator.userAgent.indexOf("Edge")<0,a=!1;function i(){return a&&o}t.hasDebuggingEnabled=i,t.attachDebuggerHotkey=function(e){a=e.some(function(e){return/\.pdb$/.test(r.getFileNameFromUrl(e))});var t=navigator.platform.match(/^Mac/i)?"Cmd":"Alt";i()&&console.info("Debugging hotkey: Shift+"+t+"+D (when application has focus)"),document.addEventListener("keydown",function(e){var t;e.shiftKey&&(e.metaKey||e.altKey)&&"KeyD"===e.code&&(a?o?((t=document.createElement("a")).href="_framework/debug?url="+encodeURIComponent(location.href),t.target="_blank",t.rel="noopener noreferrer",t.click()):console.error("Currently, only Edge(Chromium) or Chrome is supported for debugging."):console.error("Cannot start debugging, because the application was not compiled with debugging enabled."))})}},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=n(18),o=function(){function e(e){this.batchAddress=e,this.arrayRangeReader=a,this.arrayBuilderSegmentReader=i,this.diffReader=l,this.editReader=u,this.frameReader=s}return e.prototype.updatedComponents=function(){return r.platform.readStructField(this.batchAddress,0)},e.prototype.referenceFrames=function(){return r.platform.readStructField(this.batchAddress,a.structLength)},e.prototype.disposedComponentIds=function(){return r.platform.readStructField(this.batchAddress,2*a.structLength)},e.prototype.disposedEventHandlerIds=function(){return r.platform.readStructField(this.batchAddress,3*a.structLength)},e.prototype.updatedComponentsEntry=function(e,t){return c(e,t,l.structLength)},e.prototype.referenceFramesEntry=function(e,t){return c(e,t,s.structLength)},e.prototype.disposedComponentIdsEntry=function(e,t){var n=c(e,t,4);return r.platform.readInt32Field(n)},e.prototype.disposedEventHandlerIdsEntry=function(e,t){var n=c(e,t,8);return r.platform.readUint64Field(n)},e}();t.SharedMemoryRenderBatch=o;var a={structLength:8,values:function(e){return r.platform.readObjectField(e,0)},count:function(e){return r.platform.readInt32Field(e,4)}},i={structLength:12,values:function(e){var t=r.platform.readObjectField(e,0),n=r.platform.getObjectFieldsBaseAddress(t);return r.platform.readObjectField(n,0)},offset:function(e){return r.platform.readInt32Field(e,4)},count:function(e){return r.platform.readInt32Field(e,8)}},l={structLength:4+i.structLength,componentId:function(e){return r.platform.readInt32Field(e,0)},edits:function(e){return r.platform.readStructField(e,4)},editsEntry:function(e,t){return c(e,t,u.structLength)}},u={structLength:20,editType:function(e){return r.platform.readInt32Field(e,0)},siblingIndex:function(e){return r.platform.readInt32Field(e,4)},newTreeIndex:function(e){return r.platform.readInt32Field(e,8)},moveToSiblingIndex:function(e){return r.platform.readInt32Field(e,8)},removedAttributeName:function(e){return r.platform.readStringField(e,16)}},s={structLength:36,frameType:function(e){return r.platform.readInt16Field(e,4)},subtreeLength:function(e){return r.platform.readInt32Field(e,8)},elementReferenceCaptureId:function(e){return r.platform.readStringField(e,16)},componentId:function(e){return r.platform.readInt32Field(e,12)},elementName:function(e){return r.platform.readStringField(e,16)},textContent:function(e){return r.platform.readStringField(e,16)},markupContent:function(e){return r.platform.readStringField(e,16)},attributeName:function(e){return r.platform.readStringField(e,16)},attributeValue:function(e){return r.platform.readStringField(e,24)},attributeEventHandlerId:function(e){return r.platform.readUint64Field(e,8)}};function c(e,t,n){return r.platform.getArrayEntryPtr(e,t,n)}}]); \ No newline at end of file +!function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=45)}([,,,,,function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),n(25),n(18);var r=n(26),o=n(13),a={},i=!1;function u(e,t,n){var o=a[e];o||(o=a[e]=new r.BrowserRenderer(e)),o.attachRootComponentToLogicalElement(n,t)}t.attachRootComponentToLogicalElement=u,t.attachRootComponentToElement=function(e,t,n){var r=document.querySelector(e);if(!r)throw new Error("Could not find any element matching selector '"+e+"'.");u(n||0,o.toLogicalElement(r,!0),t)},t.renderBatch=function(e,t){var n=a[e];if(!n)throw new Error("There is no browser renderer with ID "+e+".");for(var r=t.arrayRangeReader,o=t.updatedComponents(),u=r.values(o),l=r.count(o),s=t.referenceFrames(),c=r.values(s),d=t.diffReader,f=0;f0&&o[o.length-1])&&(6===a[0]||2===a[0])){i=0;continue}if(3===a[0]&&(!o||a[1]>o[0]&&a[1]0&&!t)throw new Error("New logical elements must start empty, or allowExistingContents must be true");return e[r]=[],e}function u(e,t,n){var a=e;if(e instanceof Comment&&(s(a)&&s(a).length>0))throw new Error("Not implemented: inserting non-empty logical container");if(l(a))throw new Error("Not implemented: moving existing logical children");var i=s(t);if(n0;)e(r,0);var a=r;a.parentNode.removeChild(a)},t.getLogicalParent=l,t.getLogicalSiblingEnd=function(e){return e[a]||null},t.getLogicalChild=function(e,t){return s(e)[t]},t.isSvgElement=function(e){return"http://www.w3.org/2000/svg"===c(e).namespaceURI},t.getLogicalChildrenArray=s,t.permuteLogicalChildren=function(e,t){var n=s(e);t.forEach(function(e){e.moveRangeStart=n[e.fromSiblingIndex],e.moveRangeEnd=function e(t){if(t instanceof Element)return t;var n=d(t);if(n)return n.previousSibling;var r=l(t);return r instanceof Element?r.lastChild:e(r)}(e.moveRangeStart)}),t.forEach(function(t){var r=t.moveToBeforeMarker=document.createComment("marker"),o=n[t.toSiblingIndex+1];o?o.parentNode.insertBefore(r,o):f(r,e)}),t.forEach(function(e){for(var t=e.moveToBeforeMarker,n=t.parentNode,r=e.moveRangeStart,o=e.moveRangeEnd,a=r;a;){var i=a.nextSibling;if(n.insertBefore(a,t),a===o)break;a=i}n.removeChild(t)}),t.forEach(function(e){n[e.toSiblingIndex]=e.moveRangeStart})},t.getClosestDomElement=c},,,,function(e,t,n){"use strict";var r;!function(e){window.DotNet=e;var t=[],n={},r={},o=1,a=null;function i(e){t.push(e)}function u(e,t,n,r){var o=s();if(o.invokeDotNetFromJS){var a=JSON.stringify(r,h),i=o.invokeDotNetFromJS(e,t,n,a);return i?d(i):null}throw new Error("The current dispatcher does not support synchronous calls from JS to .NET. Use invokeMethodAsync instead.")}function l(e,t,r,a){if(e&&r)throw new Error("For instance method calls, assemblyName should be null. Received '"+e+"'.");var i=o++,u=new Promise(function(e,t){n[i]={resolve:e,reject:t}});try{var l=JSON.stringify(a,h);s().beginInvokeDotNetFromJS(i,e,t,r,l)}catch(e){c(i,!1,e)}return u}function s(){if(null!==a)return a;throw new Error("No .NET call dispatcher has been set.")}function c(e,t,r){if(!n.hasOwnProperty(e))throw new Error("There is no pending async call with ID "+e+".");var o=n[e];delete n[e],t?o.resolve(r):o.reject(r)}function d(e){return e?JSON.parse(e,function(e,n){return t.reduce(function(t,n){return n(e,t)},n)}):null}function f(e){return e instanceof Error?e.message+"\n"+e.stack:e?e.toString():"null"}function p(e){if(r.hasOwnProperty(e))return r[e];var t,n=window,o="window";if(e.split(".").forEach(function(e){if(!(e in n))throw new Error("Could not find '"+e+"' in '"+o+"'.");t=n,n=n[e],o+="."+e}),n instanceof Function)return n=n.bind(t),r[e]=n,n;throw new Error("The value '"+o+"' is not a function.")}e.attachDispatcher=function(e){a=e},e.attachReviver=i,e.invokeMethod=function(e,t){for(var n=[],r=2;r0&&o[o.length-1])&&(6===a[0]||2===a[0])){i=0;continue}if(3===a[0]&&(!o||a[1]>o[0]&&a[1]0&&o[o.length-1])&&(6===a[0]||2===a[0])){i=0;continue}if(3===a[0]&&(!o||a[1]>o[0]&&a[1]>2,r=Module.HEAPU32[n+1];if(r>s)throw new Error("Cannot read uint64 with high order part "+r+", because the result would exceed Number.MAX_SAFE_INTEGER.");return r*l+Module.HEAPU32[n]},readFloatField:function(e,t){return Module.getValue(e+(t||0),"float")},readObjectField:function(e,t){return Module.getValue(e+(t||0),"i32")},readStringField:function(e,n){var r=Module.getValue(e+(n||0),"i32");return 0===r?null:t.monoPlatform.toJavaScriptString(r)},readStructField:function(e,t){return e+(t||0)}};var c=document.createElement("a");function d(e){return e+12}function f(e,t,n){var r="["+e+"] "+t+":"+n;return Module.mono_bind_static_method(r)}},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=n(35),o=window.chrome&&navigator.userAgent.indexOf("Edge")<0,a=!1;function i(){return a&&o}t.hasDebuggingEnabled=i,t.attachDebuggerHotkey=function(e){a=e.some(function(e){return/\.pdb$/.test(r.getFileNameFromUrl(e))});var t=navigator.platform.match(/^Mac/i)?"Cmd":"Alt";i()&&console.info("Debugging hotkey: Shift+"+t+"+D (when application has focus)"),document.addEventListener("keydown",function(e){var t;e.shiftKey&&(e.metaKey||e.altKey)&&"KeyD"===e.code&&(a?o?((t=document.createElement("a")).href="_framework/debug?url="+encodeURIComponent(location.href),t.target="_blank",t.rel="noopener noreferrer",t.click()):console.error("Currently, only Edge(Chromium) or Chrome is supported for debugging."):console.error("Cannot start debugging, because the application was not compiled with debugging enabled."))})}},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=n(18),o=function(){function e(e){this.batchAddress=e,this.arrayRangeReader=a,this.arrayBuilderSegmentReader=i,this.diffReader=u,this.editReader=l,this.frameReader=s}return e.prototype.updatedComponents=function(){return r.platform.readStructField(this.batchAddress,0)},e.prototype.referenceFrames=function(){return r.platform.readStructField(this.batchAddress,a.structLength)},e.prototype.disposedComponentIds=function(){return r.platform.readStructField(this.batchAddress,2*a.structLength)},e.prototype.disposedEventHandlerIds=function(){return r.platform.readStructField(this.batchAddress,3*a.structLength)},e.prototype.updatedComponentsEntry=function(e,t){return c(e,t,u.structLength)},e.prototype.referenceFramesEntry=function(e,t){return c(e,t,s.structLength)},e.prototype.disposedComponentIdsEntry=function(e,t){var n=c(e,t,4);return r.platform.readInt32Field(n)},e.prototype.disposedEventHandlerIdsEntry=function(e,t){var n=c(e,t,8);return r.platform.readUint64Field(n)},e}();t.SharedMemoryRenderBatch=o;var a={structLength:8,values:function(e){return r.platform.readObjectField(e,0)},count:function(e){return r.platform.readInt32Field(e,4)}},i={structLength:12,values:function(e){var t=r.platform.readObjectField(e,0),n=r.platform.getObjectFieldsBaseAddress(t);return r.platform.readObjectField(n,0)},offset:function(e){return r.platform.readInt32Field(e,4)},count:function(e){return r.platform.readInt32Field(e,8)}},u={structLength:4+i.structLength,componentId:function(e){return r.platform.readInt32Field(e,0)},edits:function(e){return r.platform.readStructField(e,4)},editsEntry:function(e,t){return c(e,t,l.structLength)}},l={structLength:20,editType:function(e){return r.platform.readInt32Field(e,0)},siblingIndex:function(e){return r.platform.readInt32Field(e,4)},newTreeIndex:function(e){return r.platform.readInt32Field(e,8)},moveToSiblingIndex:function(e){return r.platform.readInt32Field(e,8)},removedAttributeName:function(e){return r.platform.readStringField(e,16)}},s={structLength:36,frameType:function(e){return r.platform.readInt16Field(e,4)},subtreeLength:function(e){return r.platform.readInt32Field(e,8)},elementReferenceCaptureId:function(e){return r.platform.readStringField(e,16)},componentId:function(e){return r.platform.readInt32Field(e,12)},elementName:function(e){return r.platform.readStringField(e,16)},textContent:function(e){return r.platform.readStringField(e,16)},markupContent:function(e){return r.platform.readStringField(e,16)},attributeName:function(e){return r.platform.readStringField(e,16)},attributeValue:function(e){return r.platform.readStringField(e,24)},attributeEventHandlerId:function(e){return r.platform.readUint64Field(e,8)}};function c(e,t,n){return r.platform.getArrayEntryPtr(e,t,n)}}]); \ No newline at end of file diff --git a/src/Components/Web.JS/src/Boot.WebAssembly.ts b/src/Components/Web.JS/src/Boot.WebAssembly.ts index 74bd496b02..1a80098301 100644 --- a/src/Components/Web.JS/src/Boot.WebAssembly.ts +++ b/src/Components/Web.JS/src/Boot.WebAssembly.ts @@ -2,7 +2,6 @@ import '@dotnet/jsinterop'; import './GlobalExports'; import * as Environment from './Environment'; import { monoPlatform } from './Platform/Mono/MonoPlatform'; -import { getAssemblyNameFromUrl } from './Platform/Url'; import { renderBatch } from './Rendering/Renderer'; import { SharedMemoryRenderBatch } from './Rendering/RenderBatch/SharedMemoryRenderBatch'; import { Pointer } from './Platform/Platform'; @@ -39,15 +38,13 @@ async function boot(options?: any): Promise { // Fetch the boot JSON file const bootConfig = await fetchBootConfigAsync(); - const embeddedResourcesPromise = loadEmbeddedResourcesAsync(bootConfig); if (!bootConfig.linkerEnabled) { console.info('Blazor is running in dev mode without IL stripping. To make the bundle size significantly smaller, publish the application or see https://go.microsoft.com/fwlink/?linkid=870414'); } // Determine the URLs of the assemblies we want to load, then begin fetching them all - const loadAssemblyUrls = [bootConfig.main] - .concat(bootConfig.assemblyReferences) + const loadAssemblyUrls = bootConfig.assemblies .map(filename => `_framework/_bin/${filename}`); try { @@ -56,12 +53,8 @@ async function boot(options?: any): Promise { throw new Error(`Failed to start platform. Reason: ${ex}`); } - // Before we start running .NET code, be sure embedded content resources are all loaded - await embeddedResourcesPromise; - // Start up the application - const mainAssemblyName = getAssemblyNameFromUrl(bootConfig.main); - platform.callEntryPoint(mainAssemblyName, bootConfig.entryPoint, []); + platform.callEntryPoint(bootConfig.entryAssembly); } async function fetchBootConfigAsync() { @@ -71,40 +64,16 @@ async function fetchBootConfigAsync() { return bootConfigResponse.json() as Promise; } -function loadEmbeddedResourcesAsync(bootConfig: BootJsonData): Promise { - const cssLoadingPromises = bootConfig.cssReferences.map(cssReference => { - const linkElement = document.createElement('link'); - linkElement.rel = 'stylesheet'; - linkElement.href = cssReference; - return loadResourceFromElement(linkElement); - }); - const jsLoadingPromises = bootConfig.jsReferences.map(jsReference => { - const scriptElement = document.createElement('script'); - scriptElement.src = jsReference; - return loadResourceFromElement(scriptElement); - }); - return Promise.all(cssLoadingPromises.concat(jsLoadingPromises)); -} - -function loadResourceFromElement(element: HTMLElement) { - return new Promise((resolve, reject) => { - element.onload = resolve; - element.onerror = reject; - document.head!.appendChild(element); - }); -} - // Keep in sync with BootJsonData in Microsoft.AspNetCore.Blazor.Build interface BootJsonData { - main: string; - entryPoint: string; - assemblyReferences: string[]; - cssReferences: string[]; - jsReferences: string[]; + entryAssembly: string; + assemblies: string[]; linkerEnabled: boolean; } window['Blazor'].start = boot; if (shouldAutoStart()) { - boot(); + boot().catch(error => { + Module.printErr(error); // Logs it, and causes the error UI to appear + }); } diff --git a/src/Components/Web.JS/src/Platform/Mono/MonoPlatform.ts b/src/Components/Web.JS/src/Platform/Mono/MonoPlatform.ts index 997b3d7bca..321a708f57 100644 --- a/src/Components/Web.JS/src/Platform/Mono/MonoPlatform.ts +++ b/src/Components/Web.JS/src/Platform/Mono/MonoPlatform.ts @@ -1,18 +1,9 @@ -import { MethodHandle, System_Object, System_String, System_Array, Pointer, Platform } from '../Platform'; +import { System_Object, System_String, System_Array, Pointer, Platform } from '../Platform'; import { getFileNameFromUrl } from '../Url'; import { attachDebuggerHotkey, hasDebuggingEnabled } from './MonoDebugger'; import { showErrorNotification } from '../../BootErrors'; -const assemblyHandleCache: { [assemblyName: string]: number } = {}; -const typeHandleCache: { [fullyQualifiedTypeName: string]: number } = {}; -const methodHandleCache: { [fullyQualifiedMethodName: string]: MethodHandle } = {}; - -let assembly_load: (assemblyName: string) => number; -let find_class: (assemblyHandle: number, namespace: string, className: string) => number; -let find_method: (typeHandle: number, methodName: string, unknownArg: number) => MethodHandle; -let invoke_method: (method: MethodHandle, target: System_Object, argsArrayPtr: number, exceptionFlagIntPtr: number) => System_Object; let mono_string_get_utf8: (managedString: System_String) => Mono.Utf8Ptr; -let mono_string: (jsString: string) => System_String; const appBinDirName = 'appBinDir'; const uint64HighOrderShift = Math.pow(2, 32); const maxSafeNumberHighPart = Math.pow(2, 21) - 1; // The high-order int32 from Number.MAX_SAFE_INTEGER @@ -22,7 +13,7 @@ export const monoPlatform: Platform = { return new Promise((resolve, reject) => { attachDebuggerHotkey(loadAssemblyUrls); - // mono.js assumes the existence of this + // dotnet.js assumes the existence of this window['Browser'] = { init: () => { }, }; @@ -37,52 +28,16 @@ export const monoPlatform: Platform = { }); }, - findMethod: findMethod, - - callEntryPoint: function callEntryPoint(assemblyName: string, entrypointMethod: string, args: System_Object[]): void { - // Parse the entrypointMethod, which is of the form MyApp.MyNamespace.MyTypeName::MyMethodName - // Note that we don't support specifying a method overload, so it has to be unique - const entrypointSegments = entrypointMethod.split('::'); - if (entrypointSegments.length != 2) { - throw new Error('Malformed entry point method name; could not resolve class name and method name.'); - } - const typeFullName = entrypointSegments[0]; - const methodName = entrypointSegments[1]; - const lastDot = typeFullName.lastIndexOf('.'); - const namespace = lastDot > -1 ? typeFullName.substring(0, lastDot) : ''; - const typeShortName = lastDot > -1 ? typeFullName.substring(lastDot + 1) : typeFullName; - - const entryPointMethodHandle = monoPlatform.findMethod(assemblyName, namespace, typeShortName, methodName); - monoPlatform.callMethod(entryPointMethodHandle, null, args); - }, - - callMethod: function callMethod(method: MethodHandle, target: System_Object, args: System_Object[]): System_Object { - if (args.length > 4) { - // Hopefully this restriction can be eased soon, but for now make it clear what's going on - throw new Error(`Currently, MonoPlatform supports passing a maximum of 4 arguments from JS to .NET. You tried to pass ${args.length}.`); - } - - const stack = Module.stackSave(); - - try { - const argsBuffer = Module.stackAlloc(args.length); - const exceptionFlagManagedInt = Module.stackAlloc(4); - for (let i = 0; i < args.length; ++i) { - Module.setValue(argsBuffer + i * 4, args[i], 'i32'); - } - Module.setValue(exceptionFlagManagedInt, 0, 'i32'); - - const res = invoke_method(method, target, argsBuffer, exceptionFlagManagedInt); - - if (Module.getValue(exceptionFlagManagedInt, 'i32') !== 0) { - // If the exception flag is set, the returned value is exception.ToString() - throw new Error(monoPlatform.toJavaScriptString(res)); - } - - return res; - } finally { - Module.stackRestore(stack); - } + callEntryPoint: function callEntryPoint(assemblyName: string) { + // Instead of using Module.mono_call_assembly_entry_point, we have our own logic for invoking + // the entrypoint which adds support for async main. + // Currently we disregard the return value from the entrypoint, whether it's sync or async. + // In the future, we might want Blazor.start to return a Promise>, where the + // outer promise reflects the startup process, and the inner one reflects the possibly-async + // .NET entrypoint method. + const invokeEntrypoint = bindStaticMethod('Microsoft.AspNetCore.Blazor', 'Microsoft.AspNetCore.Blazor.Hosting.EntrypointInvoker', 'InvokeEntrypoint'); + // Note we're passing in null because passing arrays is problematic until https://github.com/mono/mono/issues/18245 is resolved. + invokeEntrypoint(assemblyName, null); }, toJavaScriptString: function toJavaScriptString(managedString: System_String) { @@ -96,10 +51,6 @@ export const monoPlatform: Platform = { return res; }, - toDotNetString: function toDotNetString(jsString: string): System_String { - return mono_string(jsString); - }, - toUint8Array: function toUint8Array(array: System_Array): Uint8Array { const dataPtr = getArrayDataPointer(array); const length = Module.getValue(dataPtr, 'i32'); @@ -160,44 +111,6 @@ export const monoPlatform: Platform = { }, }; -function findAssembly(assemblyName: string): number { - let assemblyHandle = assemblyHandleCache[assemblyName]; - if (!assemblyHandle) { - assemblyHandle = assembly_load(assemblyName); - if (!assemblyHandle) { - throw new Error(`Could not find assembly "${assemblyName}"`); - } - assemblyHandleCache[assemblyName] = assemblyHandle; - } - return assemblyHandle; -} - -function findType(assemblyName: string, namespace: string, className: string): number { - const fullyQualifiedTypeName = `[${assemblyName}]${namespace}.${className}`; - let typeHandle = typeHandleCache[fullyQualifiedTypeName]; - if (!typeHandle) { - typeHandle = find_class(findAssembly(assemblyName), namespace, className); - if (!typeHandle) { - throw new Error(`Could not find type "${className}" in namespace "${namespace}" in assembly "${assemblyName}"`); - } - typeHandleCache[fullyQualifiedTypeName] = typeHandle; - } - return typeHandle; -} - -function findMethod(assemblyName: string, namespace: string, className: string, methodName: string): MethodHandle { - const fullyQualifiedMethodName = `[${assemblyName}]${namespace}.${className}::${methodName}`; - let methodHandle = methodHandleCache[fullyQualifiedMethodName]; - if (!methodHandle) { - methodHandle = find_method(findType(assemblyName, namespace, className), methodName, -1); - if (!methodHandle) { - throw new Error(`Could not find method "${methodName}" on type "${namespace}.${className}"`); - } - methodHandleCache[fullyQualifiedMethodName] = methodHandle; - } - return methodHandle; -} - function addScriptTagsToDocument() { const browserSupportsNativeWebAssembly = typeof WebAssembly !== 'undefined' && WebAssembly.validate; if (!browserSupportsNativeWebAssembly) { @@ -205,7 +118,7 @@ function addScriptTagsToDocument() { } const scriptElem = document.createElement('script'); - scriptElem.src = '_framework/wasm/mono.js'; + scriptElem.src = '_framework/wasm/dotnet.js'; scriptElem.defer = true; document.body.appendChild(scriptElem); } @@ -229,7 +142,7 @@ function addGlobalModuleScriptTagsToDocument(callback: () => void) { function createEmscriptenModuleInstance(loadAssemblyUrls: string[], onReady: () => void, onError: (reason?: any) => void) { const module = {} as typeof Module; - const wasmBinaryFile = '_framework/wasm/mono.wasm'; + const wasmBinaryFile = '_framework/wasm/dotnet.wasm'; const suppressMessages = ['DEBUGGING ENABLED']; module.print = line => (suppressMessages.indexOf(line) < 0 && console.log(`WASM: ${line}`)); @@ -244,7 +157,7 @@ function createEmscriptenModuleInstance(loadAssemblyUrls: string[], onReady: () module.locateFile = fileName => { switch (fileName) { - case 'mono.wasm': return wasmBinaryFile; + case 'dotnet.wasm': return wasmBinaryFile; default: return fileName; } }; @@ -256,24 +169,8 @@ function createEmscriptenModuleInstance(loadAssemblyUrls: string[], onReady: () 'number', 'number', ]); - assembly_load = Module.cwrap('mono_wasm_assembly_load', 'number', ['string']); - find_class = Module.cwrap('mono_wasm_assembly_find_class', 'number', [ - 'number', - 'string', - 'string', - ]); - find_method = Module.cwrap('mono_wasm_assembly_find_method', 'number', [ - 'number', - 'string', - 'number', - ]); - invoke_method = Module.cwrap('mono_wasm_invoke_method', 'number', [ - 'number', - 'number', - 'number', - ]); + mono_string_get_utf8 = Module.cwrap('mono_wasm_string_get_utf8', 'number', ['number']); - mono_string = Module.cwrap('mono_wasm_string_from_js', 'number', ['string']); MONO.loaded_files = []; @@ -346,10 +243,16 @@ function getArrayDataPointer(array: System_Array): number { return array + 12; // First byte from here is length, then following bytes are entries } +function bindStaticMethod(assembly: string, typeName: string, method: string) : (...args: any[]) => any { + // Fully qualified name looks like this: "[debugger-test] Math:IntAdd" + const fqn = `[${assembly}] ${typeName}:${method}`; + return Module.mono_bind_static_method(fqn); +} + function attachInteropInvoker(): void { - const dotNetDispatcherInvokeMethodHandle = findMethod('Mono.WebAssembly.Interop', 'Mono.WebAssembly.Interop', 'MonoWebAssemblyJSRuntime', 'InvokeDotNet'); - const dotNetDispatcherBeginInvokeMethodHandle = findMethod('Mono.WebAssembly.Interop', 'Mono.WebAssembly.Interop', 'MonoWebAssemblyJSRuntime', 'BeginInvokeDotNet'); - const dotNetDispatcherEndInvokeJSMethodHandle = findMethod('Mono.WebAssembly.Interop', 'Mono.WebAssembly.Interop', 'MonoWebAssemblyJSRuntime', 'EndInvokeJS'); + const dotNetDispatcherInvokeMethodHandle = bindStaticMethod('Mono.WebAssembly.Interop', 'Mono.WebAssembly.Interop.MonoWebAssemblyJSRuntime', 'InvokeDotNet'); + const dotNetDispatcherBeginInvokeMethodHandle = bindStaticMethod('Mono.WebAssembly.Interop', 'Mono.WebAssembly.Interop.MonoWebAssemblyJSRuntime', 'BeginInvokeDotNet'); + const dotNetDispatcherEndInvokeJSMethodHandle = bindStaticMethod('Mono.WebAssembly.Interop', 'Mono.WebAssembly.Interop.MonoWebAssemblyJSRuntime', 'EndInvokeJS'); DotNet.attachDispatcher({ beginInvokeDotNetFromJS: (callId: number, assemblyName: string | null, methodIdentifier: string, dotNetObjectId: any | null, argsJson: string): void => { @@ -362,30 +265,25 @@ function attachInteropInvoker(): void { ? dotNetObjectId.toString() : assemblyName; - monoPlatform.callMethod(dotNetDispatcherBeginInvokeMethodHandle, null, [ - callId ? monoPlatform.toDotNetString(callId.toString()) : null, - monoPlatform.toDotNetString(assemblyNameOrDotNetObjectId), - monoPlatform.toDotNetString(methodIdentifier), - monoPlatform.toDotNetString(argsJson), - ]); + dotNetDispatcherBeginInvokeMethodHandle( + callId ? callId.toString() : null, + assemblyNameOrDotNetObjectId, + methodIdentifier, + argsJson, + ); }, endInvokeJSFromDotNet: (asyncHandle, succeeded, serializedArgs): void => { - monoPlatform.callMethod( - dotNetDispatcherEndInvokeJSMethodHandle, - null, - [monoPlatform.toDotNetString(serializedArgs)] + dotNetDispatcherEndInvokeJSMethodHandle( + serializedArgs ); }, invokeDotNetFromJS: (assemblyName, methodIdentifier, dotNetObjectId, argsJson) => { - const resultJsonStringPtr = monoPlatform.callMethod(dotNetDispatcherInvokeMethodHandle, null, [ - assemblyName ? monoPlatform.toDotNetString(assemblyName) : null, - monoPlatform.toDotNetString(methodIdentifier), - dotNetObjectId ? monoPlatform.toDotNetString(dotNetObjectId.toString()) : null, - monoPlatform.toDotNetString(argsJson), - ]) as System_String; - return resultJsonStringPtr - ? monoPlatform.toJavaScriptString(resultJsonStringPtr) - : null; + return dotNetDispatcherInvokeMethodHandle( + assemblyName ? assemblyName : null, + methodIdentifier, + dotNetObjectId ? dotNetObjectId.toString() : null, + argsJson, + ) as string; }, }); } diff --git a/src/Components/Web.JS/src/Platform/Mono/MonoTypes.d.ts b/src/Components/Web.JS/src/Platform/Mono/MonoTypes.d.ts index 783af016f7..7d2f5c23bf 100644 --- a/src/Components/Web.JS/src/Platform/Mono/MonoTypes.d.ts +++ b/src/Components/Web.JS/src/Platform/Mono/MonoTypes.d.ts @@ -9,6 +9,8 @@ declare namespace Module { // These should probably be in @types/emscripten function FS_createPath(parent, path, canRead, canWrite); function FS_createDataFile(parent, name, data, canRead, canWrite, canOwn); + + function mono_bind_static_method(fqn: string): BoundStaticMethod; } // Emscripten declares these globals @@ -26,3 +28,7 @@ declare namespace MONO { var mono_wasm_runtime_is_ready: boolean; function mono_wasm_setenv (name: string, value: string): void; } + +// mono_bind_static_method allows arbitrary JS data types to be sent over the wire. However we are +// artifically limiting it to a subset of types that we actually use. +declare type BoundStaticMethod = (...args: (string | number | null)[]) => (string | number | null); diff --git a/src/Components/Web.JS/src/Platform/Platform.ts b/src/Components/Web.JS/src/Platform/Platform.ts index bb2f52113b..8d5daf454a 100644 --- a/src/Components/Web.JS/src/Platform/Platform.ts +++ b/src/Components/Web.JS/src/Platform/Platform.ts @@ -1,13 +1,9 @@ export interface Platform { start(loadAssemblyUrls: string[]): Promise; - callEntryPoint(assemblyName: string, entrypointMethod: string, args: (System_Object | null)[]); - findMethod(assemblyName: string, namespace: string, className: string, methodName: string): MethodHandle; - callMethod(method: MethodHandle, target: System_Object | null, args: (System_Object | null)[]): System_Object; + callEntryPoint(assemblyName: string): void; toJavaScriptString(dotNetString: System_String): string; - toDotNetString(javaScriptString: string): System_String; - toUint8Array(array: System_Array): Uint8Array; getArrayLength(array: System_Array): number; diff --git a/src/Components/benchmarkapps/BlazingPizza.Server/Directory.Build.props b/src/Components/benchmarkapps/BlazingPizza.Server/Directory.Build.props new file mode 100644 index 0000000000..8c119d5413 --- /dev/null +++ b/src/Components/benchmarkapps/BlazingPizza.Server/Directory.Build.props @@ -0,0 +1,2 @@ + + diff --git a/src/Components/benchmarkapps/Directory.Build.targets b/src/Components/benchmarkapps/BlazingPizza.Server/Directory.Build.targets similarity index 100% rename from src/Components/benchmarkapps/Directory.Build.targets rename to src/Components/benchmarkapps/BlazingPizza.Server/Directory.Build.targets diff --git a/src/Components/benchmarkapps/NuGet.config b/src/Components/benchmarkapps/BlazingPizza.Server/NuGet.config similarity index 100% rename from src/Components/benchmarkapps/NuGet.config rename to src/Components/benchmarkapps/BlazingPizza.Server/NuGet.config diff --git a/src/Components/benchmarkapps/Wasm.Performance/Driver/BenchmarkMeasurement.cs b/src/Components/benchmarkapps/Wasm.Performance/Driver/BenchmarkMeasurement.cs new file mode 100644 index 0000000000..62016cf630 --- /dev/null +++ b/src/Components/benchmarkapps/Wasm.Performance/Driver/BenchmarkMeasurement.cs @@ -0,0 +1,14 @@ +// 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 Wasm.Performance.Driver +{ + internal class BenchmarkMeasurement + { + public DateTime Timestamp { get; internal set; } + public string Name { get; internal set; } + public double Value { get; internal set; } + } +} \ No newline at end of file diff --git a/src/Components/benchmarkapps/Wasm.Performance/Driver/BenchmarkMetadata.cs b/src/Components/benchmarkapps/Wasm.Performance/Driver/BenchmarkMetadata.cs new file mode 100644 index 0000000000..ab98fef891 --- /dev/null +++ b/src/Components/benchmarkapps/Wasm.Performance/Driver/BenchmarkMetadata.cs @@ -0,0 +1,14 @@ +// 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 Wasm.Performance.Driver +{ + internal class BenchmarkMetadata + { + public string Source { get; set; } + public string Name { get; set; } + public string ShortDescription { get; set; } + public string LongDescription { get; set; } + public string Format { get; set; } + } +} diff --git a/src/Components/benchmarkapps/Wasm.Performance/Driver/BenchmarkOutput.cs b/src/Components/benchmarkapps/Wasm.Performance/Driver/BenchmarkOutput.cs new file mode 100644 index 0000000000..7a32ce146d --- /dev/null +++ b/src/Components/benchmarkapps/Wasm.Performance/Driver/BenchmarkOutput.cs @@ -0,0 +1,14 @@ +// 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 Wasm.Performance.Driver +{ + internal class BenchmarkOutput + { + public List Metadata { get; } = new List(); + + public List Measurements { get; } = new List(); + } +} diff --git a/src/Components/benchmarkapps/Wasm.Performance/Driver/BenchmarkResult.cs b/src/Components/benchmarkapps/Wasm.Performance/Driver/BenchmarkResult.cs new file mode 100644 index 0000000000..33e4c4094b --- /dev/null +++ b/src/Components/benchmarkapps/Wasm.Performance/Driver/BenchmarkResult.cs @@ -0,0 +1,24 @@ +namespace Wasm.Performance.Driver +{ + class BenchmarkResult + { + public string Name { get; set; } + + public BenchmarkDescriptor Descriptor { get; set; } + + public string ShortDescription { get; set; } + + public bool Success { get; set; } + + public int NumExecutions { get; set; } + + public double Duration { get; set; } + + public class BenchmarkDescriptor + { + public string Name { get; set; } + + public string Description { get; set; } + } + } +} \ No newline at end of file diff --git a/src/Components/benchmarkapps/Wasm.Performance/Driver/BenchmarkResultsStartup.cs b/src/Components/benchmarkapps/Wasm.Performance/Driver/BenchmarkResultsStartup.cs new file mode 100644 index 0000000000..7a4af028df --- /dev/null +++ b/src/Components/benchmarkapps/Wasm.Performance/Driver/BenchmarkResultsStartup.cs @@ -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.Collections.Generic; +using System.Text.Json; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Wasm.Performance.Driver +{ + public class BenchmarkDriverStartup + { + + public void ConfigureServices(IServiceCollection services) + { + services.AddCors(c => c.AddDefaultPolicy(builder => builder.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin())); + } + + public void Configure(IApplicationBuilder app) + { + app.UseCors(); + + app.Run(async context => + { + var result = await JsonSerializer.DeserializeAsync>(context.Request.Body, new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + }); + await context.Response.WriteAsync("OK"); + Program.SetBenchmarkResult(result); + }); + } + } +} diff --git a/src/Components/benchmarkapps/Wasm.Performance/Driver/Program.cs b/src/Components/benchmarkapps/Wasm.Performance/Driver/Program.cs new file mode 100644 index 0000000000..8d588b6430 --- /dev/null +++ b/src/Components/benchmarkapps/Wasm.Performance/Driver/Program.cs @@ -0,0 +1,272 @@ +// 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.IO.Compression; +using System.Linq; +using System.Runtime.ExceptionServices; +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Hosting.Server.Features; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using DevHostServerProgram = Microsoft.AspNetCore.Blazor.DevServer.Server.Program; + +namespace Wasm.Performance.Driver +{ + public class Program + { + static readonly TimeSpan Timeout = TimeSpan.FromMinutes(3); + static TaskCompletionSource> benchmarkResult = new TaskCompletionSource>(); + + public static async Task Main(string[] args) + { + var seleniumPort = 4444; + if (args.Length > 0) + { + if (!int.TryParse(args[0], out seleniumPort)) + { + Console.Error.WriteLine("Usage Driver "); + return 1; + } + } + + // This write is required for the benchmarking infrastructure. + Console.WriteLine("Application started."); + + var cancellationToken = new CancellationTokenSource(Timeout); + cancellationToken.Token.Register(() => benchmarkResult.TrySetException(new TimeoutException($"Timed out after {Timeout}"))); + + using var browser = await Selenium.CreateBrowser(seleniumPort, cancellationToken.Token); + using var testApp = StartTestApp(); + using var benchmarkReceiver = StartBenchmarkResultReceiver(); + + var testAppUrl = GetListeningUrl(testApp); + var receiverUrl = GetListeningUrl(benchmarkReceiver); + + Console.WriteLine($"Test app listening at {testAppUrl}."); + + var launchUrl = $"{testAppUrl}?resultsUrl={UrlEncoder.Default.Encode(receiverUrl)}#automated"; + browser.Url = launchUrl; + browser.Navigate(); + + var appSize = GetBlazorAppSize(); + await Task.WhenAll(benchmarkResult.Task, appSize); + FormatAsBenchmarksOutput(benchmarkResult.Task.Result, appSize.Result); + + Console.WriteLine("Done executing benchmark"); + return 0; + } + + internal static void SetBenchmarkResult(List result) + { + benchmarkResult.TrySetResult(result); + } + + private static void FormatAsBenchmarksOutput(List results, (long publishSize, long compressedSize) sizes) + { + // Sample of the the format: https://github.com/aspnet/Benchmarks/blob/e55f9e0312a7dd019d1268c1a547d1863f0c7237/src/Benchmarks/Program.cs#L51-L67 + var output = new BenchmarkOutput(); + foreach (var result in results) + { + var scenarioName = result.Descriptor.Name; + output.Metadata.Add(new BenchmarkMetadata + { + Source = "BlazorWasm", + Name = scenarioName, + ShortDescription = result.Name, + LongDescription = result.Descriptor.Description, + Format = "n2" + }); + + output.Measurements.Add(new BenchmarkMeasurement + { + Timestamp = DateTime.UtcNow, + Name = scenarioName, + Value = result.Duration, + }); + } + + // Statistics about publish sizes + output.Metadata.Add(new BenchmarkMetadata + { + Source = "BlazorWasm", + Name = "blazorwasm/publish-size", + ShortDescription = "Publish size (KB)", + LongDescription = "Publish size (KB)", + Format = "n2", + }); + + output.Measurements.Add(new BenchmarkMeasurement + { + Timestamp = DateTime.UtcNow, + Name = "blazorwasm/publish-size", + Value = sizes.publishSize / 1024, + }); + + output.Metadata.Add(new BenchmarkMetadata + { + Source = "BlazorWasm", + Name = "blazorwasm/compressed-publish-size", + ShortDescription = "Publish size compressed app (KB)", + LongDescription = "Publish size - compressed app (KB)", + Format = "n2", + }); + + output.Measurements.Add(new BenchmarkMeasurement + { + Timestamp = DateTime.UtcNow, + Name = "blazorwasm/compressed-publish-size", + Value = sizes.compressedSize / 1024, + }); + + Console.WriteLine("#StartJobStatistics"); + Console.WriteLine(JsonSerializer.Serialize(output)); + Console.WriteLine("#EndJobStatistics"); + } + + static IHost StartTestApp() + { + var args = new[] + { + "--urls", "http://127.0.0.1:0", + "--applicationpath", typeof(TestApp.Program).Assembly.Location, + }; + + var host = DevHostServerProgram.BuildWebHost(args); + RunInBackgroundThread(host.Start); + return host; + } + + static IHost StartBenchmarkResultReceiver() + { + var args = new[] + { + "--urls", "http://127.0.0.1:0", + }; + + var host = Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(builder => builder.UseStartup()) + .Build(); + + RunInBackgroundThread(host.Start); + return host; + } + + static void RunInBackgroundThread(Action action) + { + var isDone = new ManualResetEvent(false); + + ExceptionDispatchInfo edi = null; + Task.Run(() => + { + try + { + action(); + } + catch (Exception ex) + { + edi = ExceptionDispatchInfo.Capture(ex); + } + + isDone.Set(); + }); + + if (!isDone.WaitOne(Timeout)) + { + throw new TimeoutException("Timed out waiting for: " + action); + } + + if (edi != null) + { + throw edi.SourceException; + } + } + + static string GetListeningUrl(IHost testApp) + { + return testApp.Services.GetRequiredService() + .Features + .Get() + .Addresses + .First(); + } + + static async Task<(long size, long compressedSize)> GetBlazorAppSize() + { + var testAssembly = typeof(TestApp.Program).Assembly; + var testAssemblyLocation = new FileInfo(testAssembly.Location); + var testApp = new DirectoryInfo(Path.Combine( + testAssemblyLocation.Directory.FullName, + testAssembly.GetName().Name)); + + return (GetDirectorySize(testApp), await GetBrotliCompressedSize(testApp)); + } + + static long GetDirectorySize(DirectoryInfo directory) + { + // This can happen if you run the app without publishing it. + if (!directory.Exists) + { + return 0; + } + + long size = 0; + foreach (var item in directory.EnumerateFileSystemInfos()) + { + if (item is FileInfo fileInfo) + { + size += fileInfo.Length; + } + else if (item is DirectoryInfo directoryInfo) + { + size += GetDirectorySize(directoryInfo); + } + } + + return size; + } + + static async Task GetBrotliCompressedSize(DirectoryInfo directory) + { + if (!directory.Exists) + { + return 0; + } + + var tasks = new List>(); + foreach (var item in directory.EnumerateFileSystemInfos()) + { + if (item is FileInfo fileInfo) + { + tasks.Add(GetCompressedFileSize(fileInfo)); + } + else if (item is DirectoryInfo directoryInfo) + { + tasks.Add(GetBrotliCompressedSize(directoryInfo)); + } + } + + return (await Task.WhenAll(tasks)).Sum(s => s); + + async Task GetCompressedFileSize(FileInfo fileInfo) + { + using var inputStream = new FileStream(fileInfo.FullName, FileMode.Open, FileAccess.Read, FileShare.Read, 1, useAsync: true); + using var outputStream = new MemoryStream(); + + using (var brotliStream = new BrotliStream(outputStream, CompressionLevel.Optimal, leaveOpen: true)) + { + await inputStream.CopyToAsync(brotliStream); + } + + return outputStream.Length; + } + } + } +} diff --git a/src/Components/benchmarkapps/Wasm.Performance/Driver/Selenium.cs b/src/Components/benchmarkapps/Wasm.Performance/Driver/Selenium.cs new file mode 100644 index 0000000000..1c30e69e20 --- /dev/null +++ b/src/Components/benchmarkapps/Wasm.Performance/Driver/Selenium.cs @@ -0,0 +1,121 @@ +// 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.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using OpenQA.Selenium; +using OpenQA.Selenium.Chrome; +using OpenQA.Selenium.Remote; + +namespace Wasm.Performance.Driver +{ + class Selenium + { + static bool RunHeadlessBrowser = true; + static bool PoolForBrowserLogs = true; + + private static async ValueTask WaitForServerAsync(int port, CancellationToken cancellationToken) + { + var uri = new UriBuilder("http", "localhost", port, "/wd/hub/").Uri; + var httpClient = new HttpClient + { + BaseAddress = uri, + Timeout = TimeSpan.FromSeconds(1), + }; + + Console.WriteLine($"Attempting to connect to Selenium Server running at {uri}"); + + const int MaxRetries = 30; + var retries = 0; + + while (retries < MaxRetries) + { + retries++; + try + { + var response = (await httpClient.GetAsync("status", cancellationToken)).EnsureSuccessStatusCode(); + Console.WriteLine("Connected to Selenium"); + return uri; + } + catch + { + if (retries == 1) + { + Console.WriteLine("Could not connect to selenium-server. Has it been started as yet?"); + } + } + + await Task.Delay(1000); + } + + throw new Exception($"Unable to connect to selenium-server at {uri}"); + } + + public static async Task CreateBrowser(int port, CancellationToken cancellationToken) + { + var uri = await WaitForServerAsync(port, cancellationToken); + + var options = new ChromeOptions(); + + if (RunHeadlessBrowser) + { + options.AddArgument("--headless"); + } + + options.SetLoggingPreference(LogType.Browser, LogLevel.All); + + var attempt = 0; + const int MaxAttempts = 3; + do + { + try + { + // The driver opens the browser window and tries to connect to it on the constructor. + // Under heavy load, this can cause issues + // To prevent this we let the client attempt several times to connect to the server, increasing + // the max allowed timeout for a command on each attempt linearly. + var driver = new RemoteWebDriver( + uri, + options.ToCapabilities(), + TimeSpan.FromSeconds(60).Add(TimeSpan.FromSeconds(attempt * 60))); + + driver.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(1); + + if (PoolForBrowserLogs) + { + // Run in background. + var logs = new RemoteLogs(driver); + _ = Task.Run(async () => + { + while (!cancellationToken.IsCancellationRequested) + { + await Task.Delay(TimeSpan.FromSeconds(3)); + + var consoleLogs = logs.GetLog(LogType.Browser); + foreach (var entry in consoleLogs) + { + Console.WriteLine($"[Browser Log]: {entry.Timestamp}: {entry.Message}"); + } + } + }); + } + + return driver; + } + catch (Exception ex) + { + Console.WriteLine($"Error initializing RemoteWebDriver: {ex.Message}"); + } + + attempt++; + + } while (attempt < MaxAttempts); + + throw new InvalidOperationException("Couldn't create a Selenium remote driver client. The server is irresponsive"); + } + } +} diff --git a/src/Components/benchmarkapps/Wasm.Performance/Driver/Wasm.Performance.Driver.csproj b/src/Components/benchmarkapps/Wasm.Performance/Driver/Wasm.Performance.Driver.csproj new file mode 100644 index 0000000000..cf35be4e00 --- /dev/null +++ b/src/Components/benchmarkapps/Wasm.Performance/Driver/Wasm.Performance.Driver.csproj @@ -0,0 +1,23 @@ + + + + + netcoreapp3.1 + + true + exe + + + false + + + + + + + + + + + + diff --git a/src/Components/benchmarkapps/Wasm.Performance/Driver/appsettings.json b/src/Components/benchmarkapps/Wasm.Performance/Driver/appsettings.json new file mode 100644 index 0000000000..bed61b254f --- /dev/null +++ b/src/Components/benchmarkapps/Wasm.Performance/Driver/appsettings.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "IncludeScopes": false, + "LogLevel": { + "Default": "Warning" + } + } +} \ No newline at end of file diff --git a/src/Components/benchmarkapps/Wasm.Performance/README.md b/src/Components/benchmarkapps/Wasm.Performance/README.md new file mode 100644 index 0000000000..9522ecc502 --- /dev/null +++ b/src/Components/benchmarkapps/Wasm.Performance/README.md @@ -0,0 +1,20 @@ +## Blazor WASM benchmarks + +These projects assist in Benchmarking Components. +See https://github.com/aspnet/Benchmarks#benchmarks for usage guidance on using the Benchmarking tool with your application + +### Running the benchmarks + +The TestApp is a regular BlazorWASM project and can be run using `dotnet run`. The Driver is an app that connects against an existing Selenium server, and speaks the Benchmark protocol. You generally do not need to run the Driver locally, but if you were to do so, you can either start a selenium-server instance and run using `dotnet run []` or run it inside a Linux-based docker container. + +Here are the commands you would need to run it locally inside docker: + +1. `dotnet publish -c Release -r linux-x64 Driver/Wasm.Performance.Driver.csproj` +2. `docker build -t blazor-local -f ./local.dockerfile . ` +3. `docker run -it blazor-local` + +To run the benchmark app in the Benchmark server, run + +``` +dotnet run -- --config aspnetcore/src/Components/benchmarkapps/Wasm.Performance/benchmarks.compose.json application.endpoints --scenario blazorwasmbenchmark +``` diff --git a/src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/App.razor b/src/Components/benchmarkapps/Wasm.Performance/TestApp/App.razor similarity index 100% rename from src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/App.razor rename to src/Components/benchmarkapps/Wasm.Performance/TestApp/App.razor diff --git a/src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/BenchmarkEvent.cs b/src/Components/benchmarkapps/Wasm.Performance/TestApp/BenchmarkEvent.cs similarity index 89% rename from src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/BenchmarkEvent.cs rename to src/Components/benchmarkapps/Wasm.Performance/TestApp/BenchmarkEvent.cs index bdf98fd388..81cd361dce 100644 --- a/src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/BenchmarkEvent.cs +++ b/src/Components/benchmarkapps/Wasm.Performance/TestApp/BenchmarkEvent.cs @@ -3,7 +3,7 @@ using Microsoft.JSInterop; -namespace Microsoft.AspNetCore.Blazor.E2EPerformance +namespace Wasm.Performance.TestApp { public static class BenchmarkEvent { diff --git a/src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/Pages/Index.razor b/src/Components/benchmarkapps/Wasm.Performance/TestApp/Pages/Index.razor similarity index 100% rename from src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/Pages/Index.razor rename to src/Components/benchmarkapps/Wasm.Performance/TestApp/Pages/Index.razor diff --git a/src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/Pages/Json.razor b/src/Components/benchmarkapps/Wasm.Performance/TestApp/Pages/Json.razor similarity index 100% rename from src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/Pages/Json.razor rename to src/Components/benchmarkapps/Wasm.Performance/TestApp/Pages/Json.razor diff --git a/src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/Pages/RenderList.razor b/src/Components/benchmarkapps/Wasm.Performance/TestApp/Pages/RenderList.razor similarity index 100% rename from src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/Pages/RenderList.razor rename to src/Components/benchmarkapps/Wasm.Performance/TestApp/Pages/RenderList.razor diff --git a/src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/Pages/_Imports.razor b/src/Components/benchmarkapps/Wasm.Performance/TestApp/Pages/_Imports.razor similarity index 100% rename from src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/Pages/_Imports.razor rename to src/Components/benchmarkapps/Wasm.Performance/TestApp/Pages/_Imports.razor diff --git a/src/Components/benchmarkapps/Wasm.Performance/TestApp/Program.cs b/src/Components/benchmarkapps/Wasm.Performance/TestApp/Program.cs new file mode 100644 index 0000000000..57a3697382 --- /dev/null +++ b/src/Components/benchmarkapps/Wasm.Performance/TestApp/Program.cs @@ -0,0 +1,19 @@ +// 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 Microsoft.AspNetCore.Blazor.Hosting; + +namespace Wasm.Performance.TestApp +{ + public class Program + { + public static async Task Main(string[] args) + { + var builder = WebAssemblyHostBuilder.CreateDefault(args); + builder.RootComponents.Add("app"); + + await builder.Build().RunAsync(); + } + } +} diff --git a/src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/Shared/MainLayout.razor b/src/Components/benchmarkapps/Wasm.Performance/TestApp/Shared/MainLayout.razor similarity index 100% rename from src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/Shared/MainLayout.razor rename to src/Components/benchmarkapps/Wasm.Performance/TestApp/Shared/MainLayout.razor diff --git a/src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/Microsoft.AspNetCore.Blazor.E2EPerformance.csproj b/src/Components/benchmarkapps/Wasm.Performance/TestApp/Wasm.Performance.TestApp.csproj similarity index 72% rename from src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/Microsoft.AspNetCore.Blazor.E2EPerformance.csproj rename to src/Components/benchmarkapps/Wasm.Performance/TestApp/Wasm.Performance.TestApp.csproj index 9f796fdbcf..3fb5a922a3 100644 --- a/src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/Microsoft.AspNetCore.Blazor.E2EPerformance.csproj +++ b/src/Components/benchmarkapps/Wasm.Performance/TestApp/Wasm.Performance.TestApp.csproj @@ -1,7 +1,7 @@ - + - netstandard2.0 + netstandard2.1 true 3.0 diff --git a/src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/_Imports.razor b/src/Components/benchmarkapps/Wasm.Performance/TestApp/_Imports.razor similarity index 56% rename from src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/_Imports.razor rename to src/Components/benchmarkapps/Wasm.Performance/TestApp/_Imports.razor index dc263c9383..fef56339a9 100644 --- a/src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/_Imports.razor +++ b/src/Components/benchmarkapps/Wasm.Performance/TestApp/_Imports.razor @@ -2,5 +2,5 @@ @using Microsoft.AspNetCore.Components.Routing @using Microsoft.AspNetCore.Components.Web @using Microsoft.JSInterop -@using Microsoft.AspNetCore.Blazor.E2EPerformance -@using Microsoft.AspNetCore.Blazor.E2EPerformance.Shared +@using Wasm.Performance.TestApp +@using Wasm.Performance.TestApp.Shared diff --git a/src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/wwwroot/benchmarks/appStartup.js b/src/Components/benchmarkapps/Wasm.Performance/TestApp/wwwroot/benchmarks/appStartup.js similarity index 71% rename from src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/wwwroot/benchmarks/appStartup.js rename to src/Components/benchmarkapps/Wasm.Performance/TestApp/wwwroot/benchmarks/appStartup.js index 4f11b64b0f..f506b12ee4 100644 --- a/src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/wwwroot/benchmarks/appStartup.js +++ b/src/Components/benchmarkapps/Wasm.Performance/TestApp/wwwroot/benchmarks/appStartup.js @@ -10,6 +10,11 @@ group('App Startup', () => { } finally { app.dispose(); } + }, { + descriptor: { + name: "blazorwasm/time-to-first-ui", + description: "Time to render first UI (ms)" + } }); }); diff --git a/src/Components/benchmarkapps/Wasm.Performance/TestApp/wwwroot/benchmarks/index.js b/src/Components/benchmarkapps/Wasm.Performance/TestApp/wwwroot/benchmarks/index.js new file mode 100644 index 0000000000..c1690cfac8 --- /dev/null +++ b/src/Components/benchmarkapps/Wasm.Performance/TestApp/wwwroot/benchmarks/index.js @@ -0,0 +1,39 @@ +import { groups, BenchmarkEvent, onBenchmarkEvent } from './lib/minibench/minibench.js'; +import { HtmlUI } from './lib/minibench/minibench.ui.js'; +import './appStartup.js'; +import './renderList.js'; +import './jsonHandling.js'; + +new HtmlUI('E2E Performance', '#display'); + +if (location.href.indexOf('#automated') !== -1) { + const query = new URLSearchParams(window.location.search); + const group = query.get('group'); + const resultsUrl = query.get('resultsUrl'); + + groups.filter(g => !group || g.name === group).forEach(g => g.runAll()); + + const benchmarksResults = []; + onBenchmarkEvent(async (status, args) => { + switch (status) { + case BenchmarkEvent.runStarted: + benchmarksResults.length = 0; + break; + case BenchmarkEvent.benchmarkCompleted: + case BenchmarkEvent.benchmarkError: + console.log(`Completed benchmark ${args.name}`); + benchmarksResults.push(args); + break; + case BenchmarkEvent.runCompleted: + if (resultsUrl) { + await fetch(resultsUrl, { + method: 'post', + body: JSON.stringify(benchmarksResults) + }); + } + break; + default: + throw new Error(`Unknown status: ${status}`); + } + }) +} diff --git a/src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/wwwroot/benchmarks/jsonHandling.js b/src/Components/benchmarkapps/Wasm.Performance/TestApp/wwwroot/benchmarks/jsonHandling.js similarity index 66% rename from src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/wwwroot/benchmarks/jsonHandling.js rename to src/Components/benchmarkapps/Wasm.Performance/TestApp/wwwroot/benchmarks/jsonHandling.js index 4f6a311152..698eb7cee5 100644 --- a/src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/wwwroot/benchmarks/jsonHandling.js +++ b/src/Components/benchmarkapps/Wasm.Performance/TestApp/wwwroot/benchmarks/jsonHandling.js @@ -16,22 +16,47 @@ group('JSON handling', () => { teardown(() => app.dispose()); benchmark('Serialize 1kb', () => - benchmarkJson(app, '#serialize-small', '#serialized-length', 935)); + benchmarkJson(app, '#serialize-small', '#serialized-length', 935), { + descriptor: { + name: 'blazorwasm/jsonserialize-1kb', + description: 'Serialize JSON 1kb - Time in ms' + } + }); benchmark('Serialize 340kb', () => - benchmarkJson(app, '#serialize-large', '#serialized-length', 339803)); + benchmarkJson(app, '#serialize-large', '#serialized-length', 339803), { + descriptor: { + name: 'blazorwasm/jsonserialize-340kb', + description: 'Serialize JSON 340kb - Time in ms' + } + }); benchmark('Deserialize 1kb', () => - benchmarkJson(app, '#deserialize-small', '#deserialized-count', 5)); + benchmarkJson(app, '#deserialize-small', '#deserialized-count', 5), { + descriptor: { + name: 'blazorwasm/jsondeserialize-1kb', + description: 'Deserialize JSON 1kb - Time in ms' + } + }); benchmark('Deserialize 340kb', () => - benchmarkJson(app, '#deserialize-large', '#deserialized-count', 1365)); + benchmarkJson(app, '#deserialize-large', '#deserialized-count', 1365), { + descriptor: { + name: 'blazorwasm/jsondeserialize-340kb', + description: 'Deserialize JSON 340kb - Time in ms' + } + }); benchmark('Serialize 340kb (JavaScript)', () => { const json = JSON.stringify(largeObjectToSerialize); if (json.length !== 339803) { throw new Error(`Incorrect length: ${json.length}`); } + }, { + descriptor: { + name: 'blazorwasm/jsonserialize-javascript-340kb', + description: 'Serialize JSON 340kb using JavaScript - Time in ms' + } }); benchmark('Deserialize 340kb (JavaScript)', () => { @@ -39,6 +64,11 @@ group('JSON handling', () => { if (parsed.name !== 'CEO - Subordinate 0') { throw new Error('Incorrect result'); } + }, { + descriptor: { + name: 'blazorwasm/jsondeserialize-javascript-340kb', + description: 'Deserialize JSON 340kb using JavaScript - Time in ms' + } }); }); diff --git a/src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/wwwroot/benchmarks/jsonHandlingData.js b/src/Components/benchmarkapps/Wasm.Performance/TestApp/wwwroot/benchmarks/jsonHandlingData.js similarity index 100% rename from src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/wwwroot/benchmarks/jsonHandlingData.js rename to src/Components/benchmarkapps/Wasm.Performance/TestApp/wwwroot/benchmarks/jsonHandlingData.js diff --git a/src/Components/Blazor/Templates/src/content/BlazorWasm-CSharp/Client/wwwroot/css/bootstrap/bootstrap.min.css b/src/Components/benchmarkapps/Wasm.Performance/TestApp/wwwroot/benchmarks/lib/bootstrap.min.css similarity index 100% rename from src/Components/Blazor/Templates/src/content/BlazorWasm-CSharp/Client/wwwroot/css/bootstrap/bootstrap.min.css rename to src/Components/benchmarkapps/Wasm.Performance/TestApp/wwwroot/benchmarks/lib/bootstrap.min.css diff --git a/src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/wwwroot/benchmarks/lib/minibench/README.md b/src/Components/benchmarkapps/Wasm.Performance/TestApp/wwwroot/benchmarks/lib/minibench/README.md similarity index 100% rename from src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/wwwroot/benchmarks/lib/minibench/README.md rename to src/Components/benchmarkapps/Wasm.Performance/TestApp/wwwroot/benchmarks/lib/minibench/README.md diff --git a/src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/wwwroot/benchmarks/lib/minibench/minibench.js b/src/Components/benchmarkapps/Wasm.Performance/TestApp/wwwroot/benchmarks/lib/minibench/minibench.js similarity index 57% rename from src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/wwwroot/benchmarks/lib/minibench/minibench.js rename to src/Components/benchmarkapps/Wasm.Performance/TestApp/wwwroot/benchmarks/lib/minibench/minibench.js index 8214419982..f331321ad0 100644 --- a/src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/wwwroot/benchmarks/lib/minibench/minibench.js +++ b/src/Components/benchmarkapps/Wasm.Performance/TestApp/wwwroot/benchmarks/lib/minibench/minibench.js @@ -66,7 +66,7 @@ window.addEventListener('message', evt => { To work around browsers' current nonsupport for high-resolution timers (since Spectre etc.), the approach used here is to group executions into blocks of roughly fixed duration. - + - In each block, we execute the test code as many times as we can until the end of the block duration, without even yielding the thread if it's a synchronous call. We count how many executions completed. It @@ -82,7 +82,7 @@ window.addEventListener('message', evt => { during which there was no unrelated GC cycle or other background contention. - We keep running blocks until some larger timeout occurs *and* we've done at least some minimum number of executions. - + Note that this approach does *not* allow for per-execution setup/teardown logic whose timing is separated from the code under test. Because of the low timer precision, there would be no way to separate the setup duration @@ -165,7 +165,7 @@ class Benchmark extends EventEmitter { this._group = group; this.name = name; this._fn = fn; - this._options = options; + this._options = options || {}; this._state = { status: BenchmarkStatus.idle }; } @@ -174,10 +174,23 @@ class Benchmark extends EventEmitter { } run(runOptions) { + if (reportBenchmarkEvent) { + const areAllIdle = groups.reduce( + (prev, next) => prev && next.status === BenchmarkStatus.idle, + true + ); + + if (areAllIdle) { + // This is the first test being run from the idle state + reportBenchmarkEvent(BenchmarkEvent.runStarted); + } + } + this._currentRunWasAborted = false; if (this._state.status === BenchmarkStatus.idle) { this._updateState({ status: BenchmarkStatus.queued }); this.workQueueCancelHandle = addToWorkQueue(async () => { + try { if (!(runOptions && runOptions.skipGroupSetup)) { await this._group.runSetup(); @@ -192,10 +205,23 @@ class Benchmark extends EventEmitter { await this._group.runTeardown(); } + reportBenchmarkEvent(BenchmarkEvent.benchmarkCompleted, { + name: this.name, + success: true, + numExecutions: this._state.numExecutions, + duration: this._state.estimatedExecutionDurationMs, + descriptor: this._options.descriptor + }); + this._updateState({ status: BenchmarkStatus.idle }); } catch (ex) { this._updateState({ status: BenchmarkStatus.error }); console.error(ex); + reportBenchmarkEvent(BenchmarkEvent.benchmarkError, { + name: this.name, + success: false, + descriptor: this._options.descriptor + }); } }); } @@ -237,6 +263,13 @@ const BenchmarkStatus = { error: 3, }; +const BenchmarkEvent = { + runStarted: 0, + benchmarkCompleted : 1, + benchmarkError: 2, + runCompleted: 3, +} + class Group extends EventEmitter { constructor(name) { super(); @@ -279,6 +312,7 @@ class Group extends EventEmitter { } const groups = []; +let reportBenchmarkEvent = () => {}; function group(name, configure) { groups.push(new Group(name)); @@ -298,184 +332,21 @@ function teardown(fn) { groups[groups.length - 1].teardown = fn; } -class BenchmarkDisplay { - constructor(htmlUi, benchmark) { - this.benchmark = benchmark; - this.elem = document.createElement('tr'); - - const headerCol = this.elem.appendChild(document.createElement('th')); - headerCol.className = 'pl-4'; - headerCol.textContent = benchmark.name; - headerCol.setAttribute('scope', 'row'); +function onBenchmarkEvent(fn) { + reportBenchmarkEvent = fn; - const progressCol = this.elem.appendChild(document.createElement('td')); - this.numExecutionsText = progressCol.appendChild(document.createTextNode('')); + groups.forEach(group$$1 => { + group$$1.on('changed', () => { + const areAllIdle = groups.reduce( + (prev, next) => prev && next.status === BenchmarkStatus.idle, + true + ); - const timingCol = this.elem.appendChild(document.createElement('td')); - this.executionDurationText = timingCol.appendChild(document.createElement('span')); - - const runCol = this.elem.appendChild(document.createElement('td')); - runCol.className = 'pr-4'; - runCol.setAttribute('align', 'right'); - this.runButton = document.createElement('a'); - this.runButton.className = 'run-button'; - runCol.appendChild(this.runButton); - this.runButton.textContent = 'Run'; - this.runButton.onclick = evt => { - evt.preventDefault(); - this.benchmark.run(htmlUi.globalRunOptions); - }; - - benchmark.on('changed', state => this.updateDisplay(state)); - this.updateDisplay(this.benchmark.state); - } - - updateDisplay(state) { - const benchmark = this.benchmark; - this.elem.className = rowClass(state.status); - this.runButton.textContent = runButtonText(state.status); - this.numExecutionsText.textContent = state.numExecutions - ? `Executions: ${state.numExecutions}` : ''; - this.executionDurationText.innerHTML = state.estimatedExecutionDurationMs - ? `Duration: ${parseFloat(state.estimatedExecutionDurationMs.toPrecision(3))}ms` : ''; - if (state.status === BenchmarkStatus.idle) { - this.runButton.setAttribute('href', ''); - } else { - this.runButton.removeAttribute('href'); - if (state.status === BenchmarkStatus.error) { - this.numExecutionsText.textContent = 'Error - see console'; + if (areAllIdle) { + fn(BenchmarkEvent.runCompleted); } - } - } -} - -function runButtonText(status) { - switch (status) { - case BenchmarkStatus.idle: - case BenchmarkStatus.error: - return 'Run'; - case BenchmarkStatus.queued: - return 'Waiting...'; - case BenchmarkStatus.running: - return 'Running...'; - default: - throw new Error(`Unknown status: ${status}`); - } -} - -function rowClass(status) { - switch (status) { - case BenchmarkStatus.idle: - return 'benchmark-idle'; - case BenchmarkStatus.queued: - return 'benchmark-waiting'; - case BenchmarkStatus.running: - return 'benchmark-running'; - case BenchmarkStatus.error: - return 'benchmark-error'; - default: - throw new Error(`Unknown status: ${status}`); - } -} - -class GroupDisplay { - constructor(htmlUi, group) { - this.group = group; - - this.elem = document.createElement('div'); - this.elem.className = 'my-3 py-2 bg-white rounded shadow-sm'; - - const headerContainer = this.elem.appendChild(document.createElement('div')); - headerContainer.className = 'd-flex align-items-baseline px-4'; - const header = headerContainer.appendChild(document.createElement('h5')); - header.className = 'py-2'; - header.textContent = group.name; - - this.runButton = document.createElement('a'); - this.runButton.className = 'ml-auto run-button'; - this.runButton.setAttribute('href', ''); - headerContainer.appendChild(this.runButton); - this.runButton.textContent = 'Run all'; - this.runButton.onclick = evt => { - evt.preventDefault(); - group.runAll(htmlUi.globalRunOptions); - }; - - const table = this.elem.appendChild(document.createElement('table')); - table.className = 'table mb-0 benchmarks'; - const tbody = table.appendChild(document.createElement('tbody')); - - group.benchmarks.forEach(benchmark => { - const benchmarkDisplay = new BenchmarkDisplay(htmlUi, benchmark); - tbody.appendChild(benchmarkDisplay.elem); }); - - group.on('changed', () => this.updateDisplay()); - this.updateDisplay(); - } - - updateDisplay() { - const canRun = this.group.status === BenchmarkStatus.idle; - this.runButton.style.display = canRun ? 'block' : 'none'; - } -} - -class HtmlUI { - constructor(title, selector) { - this.containerElement = document.querySelector(selector); - - const headerDiv = this.containerElement.appendChild(document.createElement('div')); - headerDiv.className = 'd-flex align-items-center'; - - const header = headerDiv.appendChild(document.createElement('h2')); - header.className = 'mx-3 flex-grow-1'; - header.textContent = title; - - const verifyCheckboxLabel = document.createElement('label'); - verifyCheckboxLabel.className = 'ml-auto mr-5'; - headerDiv.appendChild(verifyCheckboxLabel); - this.verifyCheckbox = verifyCheckboxLabel.appendChild(document.createElement('input')); - this.verifyCheckbox.type = 'checkbox'; - this.verifyCheckbox.className = 'mr-2'; - verifyCheckboxLabel.appendChild(document.createTextNode('Verify only')); - - this.runButton = document.createElement('button'); - this.runButton.className = 'btn btn-success ml-auto px-4 run-button'; - headerDiv.appendChild(this.runButton); - this.runButton.textContent = 'Run all'; - this.runButton.onclick = () => { - groups.forEach(g => g.runAll(this.globalRunOptions)); - }; - - this.stopButton = document.createElement('button'); - this.stopButton.className = 'btn btn-danger ml-auto px-4 stop-button'; - headerDiv.appendChild(this.stopButton); - this.stopButton.textContent = 'Stop'; - this.stopButton.onclick = () => { - groups.forEach(g => g.stopAll()); - }; - - groups.forEach(group$$1 => { - const groupDisplay = new GroupDisplay(this, group$$1); - this.containerElement.appendChild(groupDisplay.elem); - group$$1.on('changed', () => this.updateDisplay()); - }); - - this.updateDisplay(); - } - - updateDisplay() { - const areAllIdle = groups.reduce( - (prev, next) => prev && next.status === BenchmarkStatus.idle, - true - ); - this.runButton.style.display = areAllIdle ? 'block' : 'none'; - this.stopButton.style.display = areAllIdle ? 'none' : 'block'; - } - - get globalRunOptions() { - return { verifyOnly: this.verifyCheckbox.checked }; - } + }); } /** @@ -483,4 +354,4 @@ class HtmlUI { * https://github.com/SteveSanderson/minibench */ -export { group, benchmark, setup, teardown, HtmlUI }; +export { groups, group, benchmark, setup, teardown, onBenchmarkEvent, BenchmarkEvent, BenchmarkStatus }; diff --git a/src/Components/benchmarkapps/Wasm.Performance/TestApp/wwwroot/benchmarks/lib/minibench/minibench.ui.js b/src/Components/benchmarkapps/Wasm.Performance/TestApp/wwwroot/benchmarks/lib/minibench/minibench.ui.js new file mode 100644 index 0000000000..4384b7660b --- /dev/null +++ b/src/Components/benchmarkapps/Wasm.Performance/TestApp/wwwroot/benchmarks/lib/minibench/minibench.ui.js @@ -0,0 +1,191 @@ +/** minibench - https://github.com/SteveSanderson/minibench */ + +import { groups, BenchmarkStatus } from './minibench.js'; + +class BenchmarkDisplay { + constructor(htmlUi, benchmark) { + this.benchmark = benchmark; + this.elem = document.createElement('tr'); + + const headerCol = this.elem.appendChild(document.createElement('th')); + headerCol.className = 'pl-4'; + headerCol.textContent = benchmark.name; + headerCol.setAttribute('scope', 'row'); + + const progressCol = this.elem.appendChild(document.createElement('td')); + this.numExecutionsText = progressCol.appendChild(document.createTextNode('')); + + const timingCol = this.elem.appendChild(document.createElement('td')); + this.executionDurationText = timingCol.appendChild(document.createElement('span')); + + const runCol = this.elem.appendChild(document.createElement('td')); + runCol.className = 'pr-4'; + runCol.setAttribute('align', 'right'); + this.runButton = document.createElement('a'); + this.runButton.className = 'run-button'; + runCol.appendChild(this.runButton); + this.runButton.textContent = 'Run'; + this.runButton.onclick = evt => { + evt.preventDefault(); + this.benchmark.run(htmlUi.globalRunOptions); + }; + + benchmark.on('changed', state => this.updateDisplay(state)); + this.updateDisplay(this.benchmark.state); + } + + updateDisplay(state) { + const benchmark = this.benchmark; + this.elem.className = rowClass(state.status); + this.runButton.textContent = runButtonText(state.status); + this.numExecutionsText.textContent = state.numExecutions + ? `Executions: ${state.numExecutions}` : ''; + this.executionDurationText.innerHTML = state.estimatedExecutionDurationMs + ? `Duration: ${parseFloat(state.estimatedExecutionDurationMs.toPrecision(3))}ms` : ''; + if (state.status === BenchmarkStatus.idle) { + this.runButton.setAttribute('href', ''); + } else { + this.runButton.removeAttribute('href'); + if (state.status === BenchmarkStatus.error) { + this.numExecutionsText.textContent = 'Error - see console'; + } + } + } +} + +function runButtonText(status) { + switch (status) { + case BenchmarkStatus.idle: + case BenchmarkStatus.error: + return 'Run'; + case BenchmarkStatus.queued: + return 'Waiting...'; + case BenchmarkStatus.running: + return 'Running...'; + default: + throw new Error(`Unknown status: ${status}`); + } +} + +function rowClass(status) { + switch (status) { + case BenchmarkStatus.idle: + return 'benchmark-idle'; + case BenchmarkStatus.queued: + return 'benchmark-waiting'; + case BenchmarkStatus.running: + return 'benchmark-running'; + case BenchmarkStatus.error: + return 'benchmark-error'; + default: + throw new Error(`Unknown status: ${status}`); + } +} + +class GroupDisplay { + constructor(htmlUi, group) { + this.group = group; + + this.elem = document.createElement('div'); + this.elem.className = 'my-3 py-2 bg-white rounded shadow-sm'; + + const headerContainer = this.elem.appendChild(document.createElement('div')); + headerContainer.className = 'd-flex align-items-baseline px-4'; + const header = headerContainer.appendChild(document.createElement('h5')); + header.className = 'py-2'; + header.textContent = group.name; + + this.runButton = document.createElement('a'); + this.runButton.className = 'ml-auto run-button'; + this.runButton.setAttribute('href', ''); + headerContainer.appendChild(this.runButton); + this.runButton.textContent = 'Run all'; + this.runButton.onclick = evt => { + evt.preventDefault(); + group.runAll(htmlUi.globalRunOptions); + }; + + const table = this.elem.appendChild(document.createElement('table')); + table.className = 'table mb-0 benchmarks'; + const tbody = table.appendChild(document.createElement('tbody')); + + group.benchmarks.forEach(benchmark => { + const benchmarkDisplay = new BenchmarkDisplay(htmlUi, benchmark); + tbody.appendChild(benchmarkDisplay.elem); + }); + + group.on('changed', () => this.updateDisplay()); + this.updateDisplay(); + } + + updateDisplay() { + const canRun = this.group.status === BenchmarkStatus.idle; + this.runButton.style.display = canRun ? 'block' : 'none'; + } +} + +class HtmlUI { + constructor(title, selector) { + this.containerElement = document.querySelector(selector); + + const headerDiv = this.containerElement.appendChild(document.createElement('div')); + headerDiv.className = 'd-flex align-items-center'; + + const header = headerDiv.appendChild(document.createElement('h2')); + header.className = 'mx-3 flex-grow-1'; + header.textContent = title; + + const verifyCheckboxLabel = document.createElement('label'); + verifyCheckboxLabel.className = 'ml-auto mr-5'; + headerDiv.appendChild(verifyCheckboxLabel); + this.verifyCheckbox = verifyCheckboxLabel.appendChild(document.createElement('input')); + this.verifyCheckbox.type = 'checkbox'; + this.verifyCheckbox.className = 'mr-2'; + verifyCheckboxLabel.appendChild(document.createTextNode('Verify only')); + + this.runButton = document.createElement('button'); + this.runButton.className = 'btn btn-success ml-auto px-4 run-button'; + headerDiv.appendChild(this.runButton); + this.runButton.textContent = 'Run all'; + this.runButton.setAttribute('id', 'runAll'); + this.runButton.onclick = () => { + groups.forEach(g => g.runAll(this.globalRunOptions)); + }; + + this.stopButton = document.createElement('button'); + this.stopButton.className = 'btn btn-danger ml-auto px-4 stop-button'; + headerDiv.appendChild(this.stopButton); + this.stopButton.textContent = 'Stop'; + this.stopButton.onclick = () => { + groups.forEach(g => g.stopAll()); + }; + + groups.forEach(group$$1 => { + const groupDisplay = new GroupDisplay(this, group$$1); + this.containerElement.appendChild(groupDisplay.elem); + group$$1.on('changed', () => this.updateDisplay()); + }); + + this.updateDisplay(); + } + + updateDisplay() { + const areAllIdle = groups.reduce( + (prev, next) => prev && next.status === BenchmarkStatus.idle, + true + ); + this.runButton.style.display = areAllIdle ? 'block' : 'none'; + this.stopButton.style.display = areAllIdle ? 'none' : 'block';; + } + + get globalRunOptions() { + return { verifyOnly: this.verifyCheckbox.checked }; + } +} + +/** + * minibench + * https://github.com/SteveSanderson/minibench + */ + +export { HtmlUI }; diff --git a/src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/wwwroot/benchmarks/lib/minibench/style.css b/src/Components/benchmarkapps/Wasm.Performance/TestApp/wwwroot/benchmarks/lib/minibench/style.css similarity index 100% rename from src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/wwwroot/benchmarks/lib/minibench/style.css rename to src/Components/benchmarkapps/Wasm.Performance/TestApp/wwwroot/benchmarks/lib/minibench/style.css diff --git a/src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/wwwroot/benchmarks/renderList.js b/src/Components/benchmarkapps/Wasm.Performance/TestApp/wwwroot/benchmarks/renderList.js similarity index 74% rename from src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/wwwroot/benchmarks/renderList.js rename to src/Components/benchmarkapps/Wasm.Performance/TestApp/wwwroot/benchmarks/renderList.js index 68bf32d747..32a1bb8a72 100644 --- a/src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/wwwroot/benchmarks/renderList.js +++ b/src/Components/benchmarkapps/Wasm.Performance/TestApp/wwwroot/benchmarks/renderList.js @@ -16,9 +16,24 @@ group('Rendering list', () => { app.dispose(); }); - benchmark('Render 10 items', () => measureRenderList(app, 10)); - benchmark('Render 100 items', () => measureRenderList(app, 100)); - benchmark('Render 1000 items', () => measureRenderList(app, 1000)); + benchmark('Render 10 items', () => measureRenderList(app, 10), { + descriptor: { + name: 'blazorwasm/render-10-items', + description: 'Time to render 10 item list (ms)' + } + }); + benchmark('Render 100 items', () => measureRenderList(app, 100), { + descriptor: { + name: 'blazorwasm/render-100-items', + description: 'Time to render 100 item list (ms)' + } + }); + benchmark('Render 1000 items', () => measureRenderList(app, 1000), { + descriptor: { + name: 'blazorwasm/render-1000-items', + description: 'Time to render 1000 item list (ms)' + } + }); }); diff --git a/src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/wwwroot/benchmarks/util/BenchmarkEvents.js b/src/Components/benchmarkapps/Wasm.Performance/TestApp/wwwroot/benchmarks/util/BenchmarkEvents.js similarity index 100% rename from src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/wwwroot/benchmarks/util/BenchmarkEvents.js rename to src/Components/benchmarkapps/Wasm.Performance/TestApp/wwwroot/benchmarks/util/BenchmarkEvents.js diff --git a/src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/wwwroot/benchmarks/util/BlazorApp.js b/src/Components/benchmarkapps/Wasm.Performance/TestApp/wwwroot/benchmarks/util/BlazorApp.js similarity index 100% rename from src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/wwwroot/benchmarks/util/BlazorApp.js rename to src/Components/benchmarkapps/Wasm.Performance/TestApp/wwwroot/benchmarks/util/BlazorApp.js diff --git a/src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/wwwroot/benchmarks/util/DOM.js b/src/Components/benchmarkapps/Wasm.Performance/TestApp/wwwroot/benchmarks/util/DOM.js similarity index 100% rename from src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/wwwroot/benchmarks/util/DOM.js rename to src/Components/benchmarkapps/Wasm.Performance/TestApp/wwwroot/benchmarks/util/DOM.js diff --git a/src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/wwwroot/blazor-frame.html b/src/Components/benchmarkapps/Wasm.Performance/TestApp/wwwroot/blazor-frame.html similarity index 100% rename from src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/wwwroot/blazor-frame.html rename to src/Components/benchmarkapps/Wasm.Performance/TestApp/wwwroot/blazor-frame.html diff --git a/src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/wwwroot/index.html b/src/Components/benchmarkapps/Wasm.Performance/TestApp/wwwroot/index.html similarity index 100% rename from src/Components/Blazor/testassets/Microsoft.AspNetCore.Blazor.E2EPerformance/wwwroot/index.html rename to src/Components/benchmarkapps/Wasm.Performance/TestApp/wwwroot/index.html diff --git a/src/Components/benchmarkapps/Wasm.Performance/benchmarks.compose.json b/src/Components/benchmarkapps/Wasm.Performance/benchmarks.compose.json new file mode 100644 index 0000000000..442346e79c --- /dev/null +++ b/src/Components/benchmarkapps/Wasm.Performance/benchmarks.compose.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://raw.githubusercontent.com/aspnet/Benchmarks/master/src/BenchmarksDriver2/benchmarks.schema.json", + "scenarios": { + "blazorwasmbenchmark": { + "application": { + "job": "blazorwasmbenchmark" + } + } + }, + "jobs": { + "blazorwasmbenchmark": { + "source": { + "repository": "https://github.com/dotnet/AspNetCore.git", + "branchOrCommit": "blazor-wasm", + "dockerfile": "src/Components/benchmarkapps/Wasm.Performance/dockerfile" + }, + "buildArguments": [ + "gitBranch=blazor-wasm" + ], + "waitForExit": true, + "readyStateText": "Application started." + } + } +} diff --git a/src/Components/benchmarkapps/Wasm.Performance/dockerfile b/src/Components/benchmarkapps/Wasm.Performance/dockerfile new file mode 100644 index 0000000000..69f27a9212 --- /dev/null +++ b/src/Components/benchmarkapps/Wasm.Performance/dockerfile @@ -0,0 +1,32 @@ +FROM mcr.microsoft.com/dotnet/core/sdk:3.1 AS build + +ARG DEBIAN_FRONTEND=noninteractive + +# Setup for nodejs +RUN curl -sL https://deb.nodesource.com/setup_13.x | bash - + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + libunwind-dev \ + nodejs \ + git + +ARG gitBranch=blazor-wasm + +WORKDIR /src +ADD https://api.github.com/repos/dotnet/aspnetcore/git/ref/heads/${gitBranch} /aspnetcore.commit + +RUN git init \ + && git fetch https://github.com/aspnet/aspnetcore ${gitBranch} \ + && git reset --hard FETCH_HEAD \ + && git submodule update --init + +RUN dotnet publish -c Release -r linux-x64 -o /app ./src/Components/benchmarkapps/Wasm.Performance/Driver/Wasm.Performance.Driver.csproj +RUN chmod +x /app/Wasm.Performance.Driver + +WORKDIR /app +FROM selenium/standalone-chrome:3.141.59-mercury as final +COPY --from=build ./app ./ +COPY ./exec.sh ./ + +ENTRYPOINT [ "bash", "./exec.sh" ] diff --git a/src/Components/benchmarkapps/Wasm.Performance/exec.sh b/src/Components/benchmarkapps/Wasm.Performance/exec.sh new file mode 100644 index 0000000000..bae38ae1e1 --- /dev/null +++ b/src/Components/benchmarkapps/Wasm.Performance/exec.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +/opt/bin/start-selenium-standalone.sh& +./Wasm.Performance.Driver + diff --git a/src/Components/benchmarkapps/Wasm.Performance/local.dockerfile b/src/Components/benchmarkapps/Wasm.Performance/local.dockerfile new file mode 100644 index 0000000000..188bc5dc5a --- /dev/null +++ b/src/Components/benchmarkapps/Wasm.Performance/local.dockerfile @@ -0,0 +1,7 @@ +FROM selenium/standalone-chrome:3.141.59-mercury as final + +WORKDIR /app +COPY ./Driver/bin/Release/netcoreapp3.1/linux-x64/publish ./ +COPY ./exec.sh ./ + +ENTRYPOINT [ "bash", "./exec.sh" ] diff --git a/src/Components/test/E2ETest/Infrastructure/AssemblyInfo.AssemblyFixtures.cs b/src/Components/test/E2ETest/Infrastructure/AssemblyInfo.AssemblyFixtures.cs index 9392d18aa9..4f2868269a 100644 --- a/src/Components/test/E2ETest/Infrastructure/AssemblyInfo.AssemblyFixtures.cs +++ b/src/Components/test/E2ETest/Infrastructure/AssemblyInfo.AssemblyFixtures.cs @@ -6,3 +6,4 @@ using Xunit; [assembly: TestFramework("Microsoft.AspNetCore.E2ETesting.XunitTestFrameworkWithAssemblyFixture", "Microsoft.AspNetCore.Components.E2ETests")] [assembly: AssemblyFixture(typeof(SeleniumStandaloneServer))] +[assembly: AssemblyFixture(typeof(SauceConnectServer))] diff --git a/src/Components/test/E2ETest/Infrastructure/ServerFixtures/AspNetSiteServerFixture.cs b/src/Components/test/E2ETest/Infrastructure/ServerFixtures/AspNetSiteServerFixture.cs index 98ad897bb9..abb11bc748 100644 --- a/src/Components/test/E2ETest/Infrastructure/ServerFixtures/AspNetSiteServerFixture.cs +++ b/src/Components/test/E2ETest/Infrastructure/ServerFixtures/AspNetSiteServerFixture.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; +using Microsoft.AspNetCore.E2ETesting; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Hosting; @@ -34,9 +35,15 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures var assembly = ApplicationAssembly ?? BuildWebHostMethod.Method.DeclaringType.Assembly; var sampleSitePath = FindSampleOrTestSitePath(assembly.FullName); + var host = "127.0.0.1"; + if (E2ETestOptions.Instance.SauceTest) + { + host = E2ETestOptions.Instance.Sauce.HostName; + } + return BuildWebHostMethod(new[] { - "--urls", "http://127.0.0.1:0", + "--urls", $"http://{host}:0", "--contentroot", sampleSitePath, "--environment", Environment.ToString(), }.Concat(AdditionalArguments).ToArray()); diff --git a/src/Components/test/E2ETest/Infrastructure/ServerFixtures/DevHostServerFixture.cs b/src/Components/test/E2ETest/Infrastructure/ServerFixtures/DevHostServerFixture.cs index d487598539..d09118ef3f 100644 --- a/src/Components/test/E2ETest/Infrastructure/ServerFixtures/DevHostServerFixture.cs +++ b/src/Components/test/E2ETest/Infrastructure/ServerFixtures/DevHostServerFixture.cs @@ -1,6 +1,7 @@ // 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.E2ETesting; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Http.Features; @@ -24,9 +25,15 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures ContentRoot = FindSampleOrTestSitePath( typeof(TProgram).Assembly.FullName); + var host = "127.0.0.1"; + if (E2ETestOptions.Instance.SauceTest) + { + host = E2ETestOptions.Instance.Sauce.HostName; + } + var args = new List { - "--urls", "http://127.0.0.1:0", + "--urls", $"http://{host}:0", "--contentroot", ContentRoot, "--pathbase", PathBase, "--applicationpath", typeof(TProgram).Assembly.Location, diff --git a/src/Components/test/E2ETest/Infrastructure/ServerFixtures/ServerFixture.cs b/src/Components/test/E2ETest/Infrastructure/ServerFixtures/ServerFixture.cs index 742a83f12f..67c164543c 100644 --- a/src/Components/test/E2ETest/Infrastructure/ServerFixtures/ServerFixture.cs +++ b/src/Components/test/E2ETest/Infrastructure/ServerFixtures/ServerFixture.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Reflection; using System.Runtime.ExceptionServices; using System.Threading; +using Microsoft.AspNetCore.E2ETesting; namespace Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures { @@ -22,7 +23,15 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures public ServerFixture() { _rootUriInitializer = new Lazy(() => - new Uri(StartAndGetRootUri())); + { + var uri = new Uri(StartAndGetRootUri()); + if (E2ETestOptions.Instance.SauceTest) + { + uri = new UriBuilder(uri.Scheme, E2ETestOptions.Instance.Sauce.HostName, uri.Port).Uri; + } + + return uri; + }); } public abstract void Dispose(); diff --git a/src/Components/test/E2ETest/Infrastructure/ServerFixtures/StaticSiteServerFixture.cs b/src/Components/test/E2ETest/Infrastructure/ServerFixtures/StaticSiteServerFixture.cs index 899f165e93..096e9315fe 100644 --- a/src/Components/test/E2ETest/Infrastructure/ServerFixtures/StaticSiteServerFixture.cs +++ b/src/Components/test/E2ETest/Infrastructure/ServerFixtures/StaticSiteServerFixture.cs @@ -4,6 +4,7 @@ using System; using System.IO; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.E2ETesting; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Hosting; @@ -26,13 +27,19 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures var sampleSitePath = FindSampleOrTestSitePath(SampleSiteName); + var host = "127.0.0.1"; + if (E2ETestOptions.Instance.SauceTest) + { + host = E2ETestOptions.Instance.Sauce.HostName; + } + return new HostBuilder() .ConfigureWebHost(webHostBuilder => webHostBuilder .UseKestrel() .UseContentRoot(sampleSitePath) .UseWebRoot(string.Empty) .UseStartup() - .UseUrls("http://127.0.0.1:0")) + .UseUrls($"http://{host}:0")) .Build(); } diff --git a/src/Components/test/E2ETest/Microsoft.AspNetCore.Components.E2ETests.csproj b/src/Components/test/E2ETest/Microsoft.AspNetCore.Components.E2ETests.csproj index 97c30edc5d..30eac8a9a1 100644 --- a/src/Components/test/E2ETest/Microsoft.AspNetCore.Components.E2ETests.csproj +++ b/src/Components/test/E2ETest/Microsoft.AspNetCore.Components.E2ETests.csproj @@ -7,6 +7,9 @@ $(DefaultNetCoreTargetFramework) Components.E2ETests + + true + false @@ -37,7 +40,7 @@ - + diff --git a/src/Components/test/E2ETest/Tests/ErrorNotificationServerSideTest.cs b/src/Components/test/E2ETest/ServerExecutionTests/ServerErrorNotificationTest.cs similarity index 85% rename from src/Components/test/E2ETest/Tests/ErrorNotificationServerSideTest.cs rename to src/Components/test/E2ETest/ServerExecutionTests/ServerErrorNotificationTest.cs index 6d2e860573..77b7da75d6 100644 --- a/src/Components/test/E2ETest/Tests/ErrorNotificationServerSideTest.cs +++ b/src/Components/test/E2ETest/ServerExecutionTests/ServerErrorNotificationTest.cs @@ -5,16 +5,15 @@ using BasicTestApp; using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures; using Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests; using Microsoft.AspNetCore.E2ETesting; -using OpenQA.Selenium; using Xunit; using Xunit.Abstractions; namespace Microsoft.AspNetCore.Components.E2ETest.Tests { [Collection("ErrorNotification")] // When the clientside and serverside tests run together it seems to cause failures, possibly due to connection lose on exception. - public class ErrorNotificationServerSideTest : ErrorNotificationClientSideTest + public class ServerErrorNotificationTest : ErrorNotificationTest { - public ErrorNotificationServerSideTest( + public ServerErrorNotificationTest( BrowserFixture browserFixture, ToggleExecutionModeServerFixture serverFixture, ITestOutputHelper output) diff --git a/src/Components/test/E2ETest/Tests/ErrorNotificationClientSideTest.cs b/src/Components/test/E2ETest/Tests/ErrorNotificationTest.cs similarity index 94% rename from src/Components/test/E2ETest/Tests/ErrorNotificationClientSideTest.cs rename to src/Components/test/E2ETest/Tests/ErrorNotificationTest.cs index 7c0705acde..883d1bc5ab 100644 --- a/src/Components/test/E2ETest/Tests/ErrorNotificationClientSideTest.cs +++ b/src/Components/test/E2ETest/Tests/ErrorNotificationTest.cs @@ -13,9 +13,9 @@ using Xunit.Abstractions; namespace Microsoft.AspNetCore.Components.E2ETest.Tests { [Collection("ErrorNotification")] // When the clientside and serverside tests run together it seems to cause failures, possibly due to connection lose on exception. - public class ErrorNotificationClientSideTest : ServerTestBase> + public class ErrorNotificationTest : ServerTestBase> { - public ErrorNotificationClientSideTest( + public ErrorNotificationTest( BrowserFixture browserFixture, ToggleExecutionModeServerFixture serverFixture, ITestOutputHelper output) diff --git a/src/Components/test/E2ETest/Tests/MonoSanityTest.cs b/src/Components/test/E2ETest/Tests/MonoSanityTest.cs index 1fb1b26ec9..b8db27fd2b 100644 --- a/src/Components/test/E2ETest/Tests/MonoSanityTest.cs +++ b/src/Components/test/E2ETest/Tests/MonoSanityTest.cs @@ -74,14 +74,6 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests Assert.Contains("Hello from test", GetValue(Browser, "triggerExceptionMessageStackTrace")); } - [Fact] - public void ProvidesDiagnosticIfInvokingWipedMethod() - { - Browser.FindElement(By.CssSelector("#invokeWipedMethod button")).Click(); - - Assert.Contains("System.NotImplementedException: Cannot invoke method because it was wiped. See stack trace for details.", GetValue(Browser, "invokeWipedMethodStackTrace")); - } - [Fact] public void CanCallJavaScriptFromDotNet() { diff --git a/src/Components/test/E2ETest/Tests/PerformanceTest.cs b/src/Components/test/E2ETest/Tests/PerformanceTest.cs index 652226bf26..f7187a4557 100644 --- a/src/Components/test/E2ETest/Tests/PerformanceTest.cs +++ b/src/Components/test/E2ETest/Tests/PerformanceTest.cs @@ -13,11 +13,11 @@ using Xunit.Abstractions; namespace Microsoft.AspNetCore.Components.E2ETest.Tests { public class PerformanceTest - : ServerTestBase> + : ServerTestBase> { public PerformanceTest( BrowserFixture browserFixture, - DevHostServerFixture serverFixture, + DevHostServerFixture serverFixture, ITestOutputHelper output) : base(browserFixture, serverFixture, output) { @@ -52,10 +52,8 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests () => runAllButton.Displayed || Browser.FindElements(By.CssSelector(".benchmark-error")).Any(), TimeSpan.FromSeconds(60)); - var finishedBenchmarks = Browser.FindElements(By.CssSelector(".benchmark-idle")); - var failedBenchmarks = Browser.FindElements(By.CssSelector(".benchmark-error")); - Assert.NotEmpty(finishedBenchmarks); - Assert.Empty(failedBenchmarks); + Browser.DoesNotExist(By.CssSelector(".benchmark-error")); // no failures + Browser.Exists(By.CssSelector(".benchmark-idle")); // everything's done } } } diff --git a/src/Components/test/E2ETest/Tests/StandaloneAppTest.cs b/src/Components/test/E2ETest/Tests/StandaloneAppTest.cs index db665d64c0..4cc1f57cff 100644 --- a/src/Components/test/E2ETest/Tests/StandaloneAppTest.cs +++ b/src/Components/test/E2ETest/Tests/StandaloneAppTest.cs @@ -8,7 +8,6 @@ using OpenQA.Selenium; using OpenQA.Selenium.Support.UI; using System; using System.Linq; -using System.Threading.Tasks; using Xunit; using Xunit.Abstractions; @@ -52,7 +51,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests // Verify we start at home, with the home link highlighted Assert.Equal("Hello, world!", Browser.FindElement(mainHeaderSelector).Text); Assert.Collection(Browser.FindElements(activeNavLinksSelector), - item => Assert.Equal("Home", item.Text)); + item => Assert.Equal("Home", item.Text.Trim())); // Click on the "counter" link Browser.FindElement(By.LinkText("Counter")).Click(); @@ -60,13 +59,13 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests // Verify we're now on the counter page, with that nav link (only) highlighted Assert.Equal("Counter", Browser.FindElement(mainHeaderSelector).Text); Assert.Collection(Browser.FindElements(activeNavLinksSelector), - item => Assert.Equal("Counter", item.Text)); + item => Assert.Equal("Counter", item.Text.Trim())); // Verify we can navigate back to home too Browser.FindElement(By.LinkText("Home")).Click(); Assert.Equal("Hello, world!", Browser.FindElement(mainHeaderSelector).Text); Assert.Collection(Browser.FindElements(activeNavLinksSelector), - item => Assert.Equal("Home", item.Text)); + item => Assert.Equal("Home", item.Text.Trim())); } [Fact] diff --git a/src/Components/test/E2ETest/Tests/StartupErrorNotificationTest.cs b/src/Components/test/E2ETest/Tests/StartupErrorNotificationTest.cs new file mode 100644 index 0000000000..75359253c0 --- /dev/null +++ b/src/Components/test/E2ETest/Tests/StartupErrorNotificationTest.cs @@ -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; +using BasicTestApp; +using Microsoft.AspNetCore.E2ETesting; +using Microsoft.AspNetCore.Components.E2ETest.Infrastructure; +using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures; +using OpenQA.Selenium; +using Xunit.Abstractions; +using Xunit; + +namespace Microsoft.AspNetCore.Components.E2ETest.Tests +{ + public class StartupErrorNotificationTest : ServerTestBase> + { + public StartupErrorNotificationTest( + BrowserFixture browserFixture, + DevHostServerFixture serverFixture, + ITestOutputHelper output) + : base(browserFixture, serverFixture, output) + { + _serverFixture.PathBase = ServerPathBase; + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void DisplaysNotificationForStartupException(bool errorIsAsync) + { + var url = $"{ServerPathBase}?error={(errorIsAsync ? "async" : "sync")}"; + + Navigate(url, noReload: true); + var errorUiElem = Browser.Exists(By.Id("blazor-error-ui"), TimeSpan.FromSeconds(10)); + Assert.NotNull(errorUiElem); + + Browser.Equal("block", () => errorUiElem.GetCssValue("display")); + } + } +} diff --git a/src/Components/test/E2ETest/e2eTestSettings.json b/src/Components/test/E2ETest/e2eTestSettings.json index 809f33f046..1a7155db30 100644 --- a/src/Components/test/E2ETest/e2eTestSettings.json +++ b/src/Components/test/E2ETest/e2eTestSettings.json @@ -1,4 +1,4 @@ { "DefaultWaitTimeoutInSeconds": 20, - "ScreenShotsPath": "../../screenshots" + "ScreenShotsPath": "../../screenshots", } diff --git a/src/Components/test/E2ETest/package.json b/src/Components/test/E2ETest/package.json index a84e769eb4..26a767f77b 100644 --- a/src/Components/test/E2ETest/package.json +++ b/src/Components/test/E2ETest/package.json @@ -6,11 +6,18 @@ "private": true, "scripts": { "selenium-standalone": "selenium-standalone", - "prepare": "selenium-standalone install" + "prepare": "selenium-standalone install", + "sauce": "ts-node ./scripts/sauce.ts" }, "author": "", "license": "Apache-2.0", "dependencies": { + "sauce-connect-launcher": "^1.3.1", "selenium-standalone": "^6.15.4" + }, + "devDependencies": { + "@types/node": "^13.1.7", + "ts-node": "^8.6.2", + "typescript": "^3.7.5" } } diff --git a/src/Components/test/E2ETest/scripts/sauce.ts b/src/Components/test/E2ETest/scripts/sauce.ts new file mode 100644 index 0000000000..395d0c1324 --- /dev/null +++ b/src/Components/test/E2ETest/scripts/sauce.ts @@ -0,0 +1,82 @@ +import { EOL } from "os"; +import * as _fs from "fs"; +import { promisify } from "util"; + +// Promisify things from fs we want to use. +const fs = { + createWriteStream: _fs.createWriteStream, + exists: promisify(_fs.exists), + mkdir: promisify(_fs.mkdir), + appendFile: promisify(_fs.appendFile), + readFile: promisify(_fs.readFile), +}; + +process.on("unhandledRejection", (reason) => { + console.error(`Unhandled promise rejection: ${reason}`); + process.exit(1); +}); + +let sauceUser = null; +let sauceKey = null; +let tunnelIdentifier = null; +let hostName = null; + +for (let i = 0; i < process.argv.length; i += 1) { + switch (process.argv[i]) { + case "--sauce-user": + i += 1; + sauceUser = process.argv[i]; + break; + case "--sauce-key": + i += 1; + sauceKey = process.argv[i]; + break; + case "--sauce-tunnel": + i += 1; + tunnelIdentifier = process.argv[i]; + break; + case "--use-hostname": + i += 1; + hostName = process.argv[i]; + break; + } +} + +const HOSTSFILE_PATH = process.platform === "win32" ? `${process.env.SystemRoot}\\System32\\drivers\\etc\\hosts` : null; + +(async () => { + + if (hostName) { + // Register a custom hostname in the hosts file (requires Admin, but AzDO agents run as Admin) + // Used to work around issues in Sauce Labs. + if (process.platform !== "win32") { + throw new Error("Can't use '--use-hostname' on non-Windows platform."); + } + + try { + + console.log(`Updating Hosts file (${HOSTSFILE_PATH}) to register host name '${hostName}'`); + await fs.appendFile(HOSTSFILE_PATH, `${EOL}127.0.0.1 ${hostName}${EOL}`); + + } catch (error) { + console.log(`Unable to update hosts file at ${HOSTSFILE_PATH}. Error: ${error}`); + } + } + + + // Creates a persistent proxy tunnel using Sauce Connect. + var sauceConnectLauncher = require('sauce-connect-launcher'); + + sauceConnectLauncher({ + username: sauceUser, + accessKey: sauceKey, + tunnelIdentifier: tunnelIdentifier, + }, function (err, sauceConnectProcess) { + if (err) { + console.error(err.message); + return; + } + + console.log("Sauce Connect ready"); + }); +})(); diff --git a/src/Components/test/E2ETest/yarn.lock b/src/Components/test/E2ETest/yarn.lock index 6f2b1cc3e3..250aa50786 100644 --- a/src/Components/test/E2ETest/yarn.lock +++ b/src/Components/test/E2ETest/yarn.lock @@ -2,6 +2,23 @@ # yarn lockfile v1 +"@types/node@^13.1.7": + version "13.1.8" + resolved "https://registry.yarnpkg.com/@types/node/-/node-13.1.8.tgz#1d590429fe8187a02707720ecf38a6fe46ce294b" + integrity sha512-6XzyyNM9EKQW4HKuzbo/CkOIjn/evtCmsU+MUM1xDfJ+3/rNjBttM1NgN7AOQvN6tP1Sl1D1PIKMreTArnxM9A== + +adm-zip@~0.4.3: + version "0.4.13" + resolved "https://registry.yarnpkg.com/adm-zip/-/adm-zip-0.4.13.tgz#597e2f8cc3672151e1307d3e95cddbc75672314a" + integrity sha512-fERNJX8sOXfel6qCBCMPvZLzENBEhZTzKqg6vrOW5pvoEaQuJhRU4ndTAh6lHOxn1I6jnz2NHra56ZODM751uw== + +agent-base@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.3.0.tgz#8165f01c436009bccad0b1d122f05ed770efc6ee" + integrity sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg== + dependencies: + es6-promisify "^5.0.0" + ajv@^6.5.5: version "6.10.2" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.2.tgz#d3cea04d6b017b2894ad69040fec8b623eb4bd52" @@ -12,6 +29,11 @@ ajv@^6.5.5: json-schema-traverse "^0.4.1" uri-js "^4.2.2" +arg@^4.1.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.2.tgz#e70c90579e02c63d80e3ad4e31d8bfdb8bd50064" + integrity sha512-+ytCkGcBtHZ3V2r2Z06AncYO8jz46UEamcspGoU8lHcEbpn6J77QK0vdWvChsclg/tM5XIJC5tnjmPp7Eq6Obg== + asn1@~0.2.3: version "0.2.4" resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" @@ -24,7 +46,7 @@ assert-plus@1.0.0, assert-plus@^1.0.0: resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= -async@^2.6.2: +async@^2.1.2, async@^2.6.2: version "2.6.3" resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff" integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg== @@ -46,6 +68,11 @@ aws4@^1.8.0: resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f" integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ== +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= + bcrypt-pbkdf@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" @@ -61,11 +88,24 @@ bl@^2.2.0: readable-stream "^2.3.5" safe-buffer "^5.1.1" +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + buffer-crc32@~0.2.3: version "0.2.13" resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI= +buffer-from@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" + integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== + caseless@~0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" @@ -83,6 +123,11 @@ commander@^2.19.0: resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422" integrity sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ== +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= + core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" @@ -106,6 +151,13 @@ dashdash@^1.12.0: dependencies: assert-plus "^1.0.0" +debug@^3.1.0: + version "3.2.6" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" + integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== + dependencies: + ms "^2.1.1" + debug@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" @@ -118,6 +170,11 @@ delayed-stream@~1.0.0: resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= +diff@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" + integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== + ecc-jsbn@~0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" @@ -133,6 +190,18 @@ end-of-stream@^1.4.1: dependencies: once "^1.4.0" +es6-promise@^4.0.3: + version "4.2.8" + resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" + integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w== + +es6-promisify@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203" + integrity sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM= + dependencies: + es6-promise "^4.0.3" + extend@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" @@ -184,6 +253,11 @@ fs-constants@^1.0.0: resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= + getpass@^0.1.1: version "0.1.7" resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" @@ -191,6 +265,18 @@ getpass@^0.1.1: dependencies: assert-plus "^1.0.0" +glob@^7.1.3: + version "7.1.6" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" + integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + har-schema@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" @@ -213,7 +299,23 @@ http-signature@~1.2.0: jsprim "^1.2.2" sshpk "^1.7.0" -inherits@^2.0.3, inherits@~2.0.3: +https-proxy-agent@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-3.0.1.tgz#b8c286433e87602311b01c8ea34413d856a4af81" + integrity sha512-+ML2Rbh6DAuee7d07tYGEKOEi2voWPUGan+ExdPbPW6Z3svq+JCqr0v8WmKPOkz1vOVykPCBSuobe7G8GJUtVg== + dependencies: + agent-base "^4.3.0" + debug "^3.1.0" + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@^2.0.3, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -268,11 +370,21 @@ jsprim@^1.2.2: json-schema "0.2.3" verror "1.10.0" +lodash@^4.16.6: + version "4.17.15" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" + integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== + lodash@^4.17.11, lodash@^4.17.14: version "4.17.14" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.14.tgz#9ce487ae66c96254fe20b599f21b6816028078ba" integrity sha512-mmKYbW3GLuJeX+iGP+Y7Gp1AiGHGbXHCOh/jZmrawMmsE7MS4znI3RL2FsjbqOyMayHInjOeykW7PEajUk1/xw== +make-error@^1.1.1: + version "1.3.5" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.5.tgz#efe4e81f6db28cadd605c70f29c831b58ef776c8" + integrity sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g== + mime-db@1.40.0: version "1.40.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.40.0.tgz#a65057e998db090f732a68f6c276d387d4126c32" @@ -285,6 +397,13 @@ mime-types@^2.1.12, mime-types@~2.1.19: dependencies: mime-db "1.40.0" +minimatch@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== + dependencies: + brace-expansion "^1.1.7" + minimist@0.0.8: version "0.0.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" @@ -317,13 +436,18 @@ oauth-sign@~0.9.0: resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== -once@^1.4.0: +once@^1.3.0, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= dependencies: wrappy "1" +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= + path-key@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" @@ -417,6 +541,13 @@ request@2.88.0: tunnel-agent "^0.6.0" uuid "^3.3.2" +rimraf@^2.5.4: + version "2.7.1" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" + integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== + dependencies: + glob "^7.1.3" + safe-buffer@^5.0.1, safe-buffer@^5.1.1, safe-buffer@^5.1.2: version "5.2.0" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519" @@ -432,6 +563,17 @@ safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== +sauce-connect-launcher@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/sauce-connect-launcher/-/sauce-connect-launcher-1.3.1.tgz#31137f57b0f7176e1c0525b7fb09c6da746647cf" + integrity sha512-vIf9qDol3q2FlYzrKt0dr3kvec6LSjX2WS+/mVnAJIhqh1evSkPKCR2AzcJrnSmx9Xt9PtV0tLY7jYh0wsQi8A== + dependencies: + adm-zip "~0.4.3" + async "^2.1.2" + https-proxy-agent "^3.0.0" + lodash "^4.16.6" + rimraf "^2.5.4" + selenium-standalone@^6.15.4: version "6.16.0" resolved "https://registry.yarnpkg.com/selenium-standalone/-/selenium-standalone-6.16.0.tgz#ffcf02665c58ff7a7472427ae819ba79c15967ac" @@ -468,6 +610,19 @@ shebang-regex@^1.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= +source-map-support@^0.5.6: + version "0.5.16" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.16.tgz#0ae069e7fe3ba7538c64c98515e35339eac5a042" + integrity sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map@^0.6.0: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + sshpk@^1.7.0: version "1.16.1" resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877" @@ -516,6 +671,17 @@ tough-cookie@~2.4.3: psl "^1.1.24" punycode "^1.4.1" +ts-node@^8.6.2: + version "8.6.2" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-8.6.2.tgz#7419a01391a818fbafa6f826a33c1a13e9464e35" + integrity sha512-4mZEbofxGqLL2RImpe3zMJukvEvcO1XP8bj8ozBPySdCUXEcU5cIRwR0aM3R+VoZq7iXc8N86NC0FspGRqP4gg== + dependencies: + arg "^4.1.0" + diff "^4.0.1" + make-error "^1.1.1" + source-map-support "^0.5.6" + yn "3.1.1" + tunnel-agent@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" @@ -528,6 +694,11 @@ tweetnacl@^0.14.3, tweetnacl@~0.14.0: resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= +typescript@^3.7.5: + version "3.7.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.7.5.tgz#0692e21f65fd4108b9330238aac11dd2e177a1ae" + integrity sha512-/P5lkRXkWHNAbcJIiHPfRoKqyd7bsyCma1hZNUGfn20qm64T6ZBlrzprymeu918H+mB/0rIg2gGK/BXkhhYgBw== + uri-js@^4.2.2: version "4.2.2" resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" @@ -578,3 +749,8 @@ yauzl@^2.10.0: dependencies: buffer-crc32 "~0.2.3" fd-slicer "~1.1.0" + +yn@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" + integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== diff --git a/src/Components/test/testassets/BasicTestApp/BasicTestApp.csproj b/src/Components/test/testassets/BasicTestApp/BasicTestApp.csproj index 98357d0e88..9914ec4521 100644 --- a/src/Components/test/testassets/BasicTestApp/BasicTestApp.csproj +++ b/src/Components/test/testassets/BasicTestApp/BasicTestApp.csproj @@ -1,7 +1,7 @@ - netstandard2.0 + netstandard2.1 3.0 true diff --git a/src/Components/test/testassets/BasicTestApp/Index.razor b/src/Components/test/testassets/BasicTestApp/Index.razor index 0548f382a8..eff29276a4 100644 --- a/src/Components/test/testassets/BasicTestApp/Index.razor +++ b/src/Components/test/testassets/BasicTestApp/Index.razor @@ -87,12 +87,6 @@ @((RenderFragment)RenderSelectedComponent)
-
- An unhandled error has occurred. - Reload - 🗙 -
- @code { string SelectedComponentTypeName { get; set; } = "none"; diff --git a/src/Components/test/testassets/BasicTestApp/Program.cs b/src/Components/test/testassets/BasicTestApp/Program.cs index cebb226e7c..0c62f05bd1 100644 --- a/src/Components/test/testassets/BasicTestApp/Program.cs +++ b/src/Components/test/testassets/BasicTestApp/Program.cs @@ -1,23 +1,64 @@ // 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.Globalization; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using BasicTestApp.AuthTest; using Microsoft.AspNetCore.Blazor.Hosting; +using Microsoft.AspNetCore.Blazor.Http; +using Microsoft.AspNetCore.Components.Authorization; +using Microsoft.Extensions.DependencyInjection; +using Mono.WebAssembly.Interop; namespace BasicTestApp { public class Program { - public static void Main(string[] args) + public static async Task Main(string[] args) { + await SimulateErrorsIfNeededForTest(); + // We want the culture to be en-US so that the tests for bind can work consistently. CultureInfo.CurrentCulture = new CultureInfo("en-US"); - CreateHostBuilder(args).Build().Run(); + var builder = WebAssemblyHostBuilder.CreateDefault(args); + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Create("WEBASSEMBLY"))) + { + // Needed because the test server runs on a different port than the client app, + // and we want to test sending/receiving cookies under this config + WebAssemblyHttpMessageHandlerOptions.DefaultCredentials = FetchCredentialsOption.Include; + } + + builder.RootComponents.Add("root"); + + builder.Services.AddSingleton(); + builder.Services.AddAuthorizationCore(options => + { + options.AddPolicy("NameMustStartWithB", policy => + policy.RequireAssertion(ctx => ctx.User.Identity.Name?.StartsWith("B") ?? false)); + }); + + await builder.Build().RunAsync(); } - public static IWebAssemblyHostBuilder CreateHostBuilder(string[] args) => - BlazorWebAssemblyHost.CreateDefaultBuilder() - .UseBlazorStartup(); + // Supports E2E tests in StartupErrorNotificationTest + private static async Task SimulateErrorsIfNeededForTest() + { + var currentUrl = new MonoWebAssemblyJSRuntime().Invoke("getCurrentUrl"); + if (currentUrl.Contains("error=sync")) + { + throw new InvalidTimeZoneException("This is a synchronous startup exception"); + } + + await Task.Yield(); + + if (currentUrl.Contains("error=async")) + { + throw new InvalidTimeZoneException("This is an asynchronous startup exception"); + } + } } } diff --git a/src/Components/test/testassets/BasicTestApp/Startup.cs b/src/Components/test/testassets/BasicTestApp/Startup.cs deleted file mode 100644 index 008a988316..0000000000 --- a/src/Components/test/testassets/BasicTestApp/Startup.cs +++ /dev/null @@ -1,39 +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.Runtime.InteropServices; -using BasicTestApp.AuthTest; -using Microsoft.AspNetCore.Blazor.Http; -using Microsoft.AspNetCore.Components; -using Microsoft.AspNetCore.Components.Authorization; -using Microsoft.AspNetCore.Components.Builder; -using Microsoft.Extensions.DependencyInjection; - -namespace BasicTestApp -{ - public class Startup - { - public void ConfigureServices(IServiceCollection services) - { - services.AddSingleton(); - - services.AddAuthorizationCore(options => - { - options.AddPolicy("NameMustStartWithB", policy => - policy.RequireAssertion(ctx => ctx.User.Identity.Name?.StartsWith("B") ?? false)); - }); - } - - public void Configure(IComponentsApplicationBuilder app) - { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Create("WEBASSEMBLY"))) - { - // Needed because the test server runs on a different port than the client app, - // and we want to test sending/receiving cookies underling this config - WebAssemblyHttpMessageHandlerOptions.DefaultCredentials = FetchCredentialsOption.Include; - } - - app.AddComponent("root"); - } - } -} diff --git a/src/Components/test/testassets/BasicTestApp/wwwroot/index.html b/src/Components/test/testassets/BasicTestApp/wwwroot/index.html index 517d9d6fcc..a37c08a7d1 100644 --- a/src/Components/test/testassets/BasicTestApp/wwwroot/index.html +++ b/src/Components/test/testassets/BasicTestApp/wwwroot/index.html @@ -14,6 +14,13 @@ Loading... + + + @@ -27,6 +34,10 @@ function navigationManagerNavigate() { Blazor.navigateTo('/subdir/some-path'); } + + function getCurrentUrl() { + return location.href; + } diff --git a/src/Components/test/testassets/BasicTestApp/wwwroot/style.css b/src/Components/test/testassets/BasicTestApp/wwwroot/style.css index 777375d9e0..ea9900430b 100644 --- a/src/Components/test/testassets/BasicTestApp/wwwroot/style.css +++ b/src/Components/test/testassets/BasicTestApp/wwwroot/style.css @@ -7,11 +7,23 @@ } #blazor-error-ui { + background: lightyellow; + bottom: 0; + box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); display: none; + left: 0; + padding: 0.6rem 1.25rem 0.7rem 1.25rem; + position: fixed; + width: 100%; + z-index: 1000; + box-sizing: border-box; } - #blazor-error-ui dismiss { + #blazor-error-ui .dismiss { cursor: pointer; + position: absolute; + right: 0.75rem; + top: 0.5rem; } .validation-message { diff --git a/src/Components/test/testassets/TestServer/CorsStartup.cs b/src/Components/test/testassets/TestServer/CorsStartup.cs index a4d91a84aa..28dda32d90 100644 --- a/src/Components/test/testassets/TestServer/CorsStartup.cs +++ b/src/Components/test/testassets/TestServer/CorsStartup.cs @@ -46,7 +46,7 @@ namespace TestServer app.Map("/subdir", app => { app.UseStaticFiles(); - app.UseClientSideBlazorFiles(); + app.UseClientSideBlazorFiles(); app.UseRouting(); @@ -55,7 +55,7 @@ namespace TestServer app.UseEndpoints(endpoints => { endpoints.MapControllers(); - endpoints.MapFallbackToClientSideBlazor("index.html"); + endpoints.MapFallbackToClientSideBlazor("index.html"); }); }); } diff --git a/src/Components/test/testassets/TestServer/InternationalizationStartup.cs b/src/Components/test/testassets/TestServer/InternationalizationStartup.cs index d508ed797b..7521ebe34b 100644 --- a/src/Components/test/testassets/TestServer/InternationalizationStartup.cs +++ b/src/Components/test/testassets/TestServer/InternationalizationStartup.cs @@ -37,7 +37,7 @@ namespace TestServer app.Map("/subdir", app => { app.UseStaticFiles(); - app.UseClientSideBlazorFiles(); + app.UseClientSideBlazorFiles(); app.UseRequestLocalization(options => { diff --git a/src/Components/test/testassets/TestServer/Pages/_ServerHost.cshtml b/src/Components/test/testassets/TestServer/Pages/_ServerHost.cshtml index af2f28f658..b0ba837af2 100644 --- a/src/Components/test/testassets/TestServer/Pages/_ServerHost.cshtml +++ b/src/Components/test/testassets/TestServer/Pages/_ServerHost.cshtml @@ -17,6 +17,12 @@ +
+ An unhandled error has occurred. + Reload + 🗙 +
+