diff --git a/AspNetCore.sln b/AspNetCore.sln index aea9643f478f981430198697cef78ca9019c2a76..2f12c219cd0deb87fe56cae4fc8c26192662d49a 100644 --- a/AspNetCore.sln +++ b/AspNetCore.sln @@ -1427,6 +1427,16 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Compon EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Components.Web.Extensions.Tests", "src\Components\Web.Extensions\test\Microsoft.AspNetCore.Components.Web.Extensions.Tests.csproj", "{157605CB-5170-4C1A-980F-4BAE42DB60DE}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Sdk", "Sdk", "{FED4267E-E5E4-49C5-98DB-8B3F203596EE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.NET.Sdk.BlazorWebAssembly", "src\Components\WebAssembly\Sdk\src\Microsoft.NET.Sdk.BlazorWebAssembly.csproj", "{6B2734BF-C61D-4889-ABBF-456A4075D59B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.NET.Sdk.BlazorWebAssembly.Tests", "src\Components\WebAssembly\Sdk\test\Microsoft.NET.Sdk.BlazorWebAssembly.Tests.csproj", "{83371889-9A3E-4D16-AE77-EB4F83BC6374}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.NET.Sdk.BlazorWebAssembly.IntegrationTests", "src\Components\WebAssembly\Sdk\integrationtests\Microsoft.NET.Sdk.BlazorWebAssembly.IntegrationTests.csproj", "{525EBCB4-A870-470B-BC90-845306C337D1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.NET.Sdk.BlazorWebAssembly.Tools", "src\Components\WebAssembly\Sdk\tools\Microsoft.NET.Sdk.BlazorWebAssembly.Tools.csproj", "{175E5CD8-92D4-46BB-882E-3A930D3302D4}" +EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "testassets", "testassets", "{6126DCE4-9692-4EE2-B240-C65743572995}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BasicTestApp", "src\Components\test\testassets\BasicTestApp\BasicTestApp.csproj", "{46FB7E93-1294-4068-B80A-D4864F78277A}" @@ -6743,6 +6753,54 @@ Global {157605CB-5170-4C1A-980F-4BAE42DB60DE}.Release|x64.Build.0 = Release|Any CPU {157605CB-5170-4C1A-980F-4BAE42DB60DE}.Release|x86.ActiveCfg = Release|Any CPU {157605CB-5170-4C1A-980F-4BAE42DB60DE}.Release|x86.Build.0 = Release|Any CPU + {6B2734BF-C61D-4889-ABBF-456A4075D59B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6B2734BF-C61D-4889-ABBF-456A4075D59B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6B2734BF-C61D-4889-ABBF-456A4075D59B}.Debug|x64.ActiveCfg = Debug|Any CPU + {6B2734BF-C61D-4889-ABBF-456A4075D59B}.Debug|x64.Build.0 = Debug|Any CPU + {6B2734BF-C61D-4889-ABBF-456A4075D59B}.Debug|x86.ActiveCfg = Debug|Any CPU + {6B2734BF-C61D-4889-ABBF-456A4075D59B}.Debug|x86.Build.0 = Debug|Any CPU + {6B2734BF-C61D-4889-ABBF-456A4075D59B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6B2734BF-C61D-4889-ABBF-456A4075D59B}.Release|Any CPU.Build.0 = Release|Any CPU + {6B2734BF-C61D-4889-ABBF-456A4075D59B}.Release|x64.ActiveCfg = Release|Any CPU + {6B2734BF-C61D-4889-ABBF-456A4075D59B}.Release|x64.Build.0 = Release|Any CPU + {6B2734BF-C61D-4889-ABBF-456A4075D59B}.Release|x86.ActiveCfg = Release|Any CPU + {6B2734BF-C61D-4889-ABBF-456A4075D59B}.Release|x86.Build.0 = Release|Any CPU + {83371889-9A3E-4D16-AE77-EB4F83BC6374}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {83371889-9A3E-4D16-AE77-EB4F83BC6374}.Debug|Any CPU.Build.0 = Debug|Any CPU + {83371889-9A3E-4D16-AE77-EB4F83BC6374}.Debug|x64.ActiveCfg = Debug|Any CPU + {83371889-9A3E-4D16-AE77-EB4F83BC6374}.Debug|x64.Build.0 = Debug|Any CPU + {83371889-9A3E-4D16-AE77-EB4F83BC6374}.Debug|x86.ActiveCfg = Debug|Any CPU + {83371889-9A3E-4D16-AE77-EB4F83BC6374}.Debug|x86.Build.0 = Debug|Any CPU + {83371889-9A3E-4D16-AE77-EB4F83BC6374}.Release|Any CPU.ActiveCfg = Release|Any CPU + {83371889-9A3E-4D16-AE77-EB4F83BC6374}.Release|Any CPU.Build.0 = Release|Any CPU + {83371889-9A3E-4D16-AE77-EB4F83BC6374}.Release|x64.ActiveCfg = Release|Any CPU + {83371889-9A3E-4D16-AE77-EB4F83BC6374}.Release|x64.Build.0 = Release|Any CPU + {83371889-9A3E-4D16-AE77-EB4F83BC6374}.Release|x86.ActiveCfg = Release|Any CPU + {83371889-9A3E-4D16-AE77-EB4F83BC6374}.Release|x86.Build.0 = Release|Any CPU + {525EBCB4-A870-470B-BC90-845306C337D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {525EBCB4-A870-470B-BC90-845306C337D1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {525EBCB4-A870-470B-BC90-845306C337D1}.Debug|x64.ActiveCfg = Debug|Any CPU + {525EBCB4-A870-470B-BC90-845306C337D1}.Debug|x64.Build.0 = Debug|Any CPU + {525EBCB4-A870-470B-BC90-845306C337D1}.Debug|x86.ActiveCfg = Debug|Any CPU + {525EBCB4-A870-470B-BC90-845306C337D1}.Debug|x86.Build.0 = Debug|Any CPU + {525EBCB4-A870-470B-BC90-845306C337D1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {525EBCB4-A870-470B-BC90-845306C337D1}.Release|Any CPU.Build.0 = Release|Any CPU + {525EBCB4-A870-470B-BC90-845306C337D1}.Release|x64.ActiveCfg = Release|Any CPU + {525EBCB4-A870-470B-BC90-845306C337D1}.Release|x64.Build.0 = Release|Any CPU + {525EBCB4-A870-470B-BC90-845306C337D1}.Release|x86.ActiveCfg = Release|Any CPU + {525EBCB4-A870-470B-BC90-845306C337D1}.Release|x86.Build.0 = Release|Any CPU + {175E5CD8-92D4-46BB-882E-3A930D3302D4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {175E5CD8-92D4-46BB-882E-3A930D3302D4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {175E5CD8-92D4-46BB-882E-3A930D3302D4}.Debug|x64.ActiveCfg = Debug|Any CPU + {175E5CD8-92D4-46BB-882E-3A930D3302D4}.Debug|x64.Build.0 = Debug|Any CPU + {175E5CD8-92D4-46BB-882E-3A930D3302D4}.Debug|x86.ActiveCfg = Debug|Any CPU + {175E5CD8-92D4-46BB-882E-3A930D3302D4}.Debug|x86.Build.0 = Debug|Any CPU + {175E5CD8-92D4-46BB-882E-3A930D3302D4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {175E5CD8-92D4-46BB-882E-3A930D3302D4}.Release|Any CPU.Build.0 = Release|Any CPU + {175E5CD8-92D4-46BB-882E-3A930D3302D4}.Release|x64.ActiveCfg = Release|Any CPU + {175E5CD8-92D4-46BB-882E-3A930D3302D4}.Release|x64.Build.0 = Release|Any CPU + {175E5CD8-92D4-46BB-882E-3A930D3302D4}.Release|x86.ActiveCfg = Release|Any CPU + {175E5CD8-92D4-46BB-882E-3A930D3302D4}.Release|x86.Build.0 = Release|Any CPU {46FB7E93-1294-4068-B80A-D4864F78277A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {46FB7E93-1294-4068-B80A-D4864F78277A}.Debug|Any CPU.Build.0 = Debug|Any CPU {46FB7E93-1294-4068-B80A-D4864F78277A}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -7530,6 +7588,11 @@ Global {F71FE795-9923-461B-9809-BB1821A276D0} = {60D51C98-2CC0-40DF-B338-44154EFEE2FF} {8294A74F-7DAA-4B69-BC56-7634D93C9693} = {F71FE795-9923-461B-9809-BB1821A276D0} {157605CB-5170-4C1A-980F-4BAE42DB60DE} = {F71FE795-9923-461B-9809-BB1821A276D0} + {FED4267E-E5E4-49C5-98DB-8B3F203596EE} = {562D5067-8CD8-4F19-BCBB-873204932C61} + {6B2734BF-C61D-4889-ABBF-456A4075D59B} = {FED4267E-E5E4-49C5-98DB-8B3F203596EE} + {83371889-9A3E-4D16-AE77-EB4F83BC6374} = {FED4267E-E5E4-49C5-98DB-8B3F203596EE} + {525EBCB4-A870-470B-BC90-845306C337D1} = {FED4267E-E5E4-49C5-98DB-8B3F203596EE} + {175E5CD8-92D4-46BB-882E-3A930D3302D4} = {FED4267E-E5E4-49C5-98DB-8B3F203596EE} {6126DCE4-9692-4EE2-B240-C65743572995} = {0508E463-0269-40C9-B5C2-3B600FB2A28B} {46FB7E93-1294-4068-B80A-D4864F78277A} = {6126DCE4-9692-4EE2-B240-C65743572995} {25FA84DB-EEA7-4068-8E2D-F3D48B281C16} = {6126DCE4-9692-4EE2-B240-C65743572995} diff --git a/eng/Build.props b/eng/Build.props index 56b4917250d150b4d20c053ad50fcd9e7fcf63f6..3740546dd641018a5cd819b9d08f3fe631e94e58 100644 --- a/eng/Build.props +++ b/eng/Build.props @@ -172,6 +172,7 @@ @(ProjectToBuild); @(ProjectToExclude); $(RepoRoot)src\Razor\test\testassets\**\*.*proj; + $(RepoRoot)src\Components\WebAssembly\Sdk\testassets\**\*.*proj; $(RepoRoot)**\node_modules\**\*; $(RepoRoot)**\bin\**\*; $(RepoRoot)**\obj\**\*;" diff --git a/eng/ProjectReferences.props b/eng/ProjectReferences.props index a0e89782caeeb3f292da48a893b1cefbf3b074fd..6dbfca4646bdd90fabdd0e10140852287365ab67 100644 --- a/eng/ProjectReferences.props +++ b/eng/ProjectReferences.props @@ -145,6 +145,7 @@ <ProjectReferenceProvider Include="Microsoft.AspNetCore.Components.Web.Extensions" ProjectPath="$(RepoRoot)src\Components\Web.Extensions\src\Microsoft.AspNetCore.Components.Web.Extensions.csproj" /> <ProjectReferenceProvider Include="Microsoft.Authentication.WebAssembly.Msal" ProjectPath="$(RepoRoot)src\Components\WebAssembly\Authentication.Msal\src\Microsoft.Authentication.WebAssembly.Msal.csproj" /> <ProjectReferenceProvider Include="Microsoft.JSInterop.WebAssembly" ProjectPath="$(RepoRoot)src\Components\WebAssembly\JSInterop\src\Microsoft.JSInterop.WebAssembly.csproj" /> + <ProjectReferenceProvider Include="Microsoft.NET.Sdk.BlazorWebAssembly" ProjectPath="$(RepoRoot)src\Components\WebAssembly\Sdk\src\Microsoft.NET.Sdk.BlazorWebAssembly.csproj" /> <ProjectReferenceProvider Include="Microsoft.AspNetCore.Components.WebAssembly.Server" ProjectPath="$(RepoRoot)src\Components\WebAssembly\Server\src\Microsoft.AspNetCore.Components.WebAssembly.Server.csproj" /> <ProjectReferenceProvider Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" ProjectPath="$(RepoRoot)src\Components\WebAssembly\WebAssembly.Authentication\src\Microsoft.AspNetCore.Components.WebAssembly.Authentication.csproj" /> <ProjectReferenceProvider Include="Microsoft.AspNetCore.Components.WebAssembly" ProjectPath="$(RepoRoot)src\Components\WebAssembly\WebAssembly\src\Microsoft.AspNetCore.Components.WebAssembly.csproj" /> diff --git a/src/Components/Components.slnf b/src/Components/Components.slnf index c202f7c1c69ba1b663e1059b6f9609ddf827ed8a..637ec1a3845896cb970d59e95ebec98aee088bc4 100644 --- a/src/Components/Components.slnf +++ b/src/Components/Components.slnf @@ -110,6 +110,9 @@ "src\\Components\\WebAssembly\\WebAssembly.Authentication\\test\\Microsoft.AspNetCore.Components.WebAssembly.Authentication.Tests.csproj", "src\\Components\\WebAssembly\\testassets\\Wasm.Authentication.Client\\Wasm.Authentication.Client.csproj", "src\\Components\\WebAssembly\\testassets\\Wasm.Authentication.Shared\\Wasm.Authentication.Shared.csproj", + "src\\Components\\WebAssembly\\Sdk\\src\\Microsoft.NET.Sdk.BlazorWebAssembly.csproj", + "src\\Components\\WebAssembly\\Sdk\\test\\Microsoft.NET.Sdk.BlazorWebAssembly.Tests.csproj", + "src\\Components\\WebAssembly\\Sdk\\integrationtests\\Microsoft.NET.Sdk.BlazorWebAssembly.IntegrationTests.csproj", "src\\JSInterop\\Microsoft.JSInterop\\src\\Microsoft.JSInterop.csproj" ] } diff --git a/src/Components/Components/src/Routing/Router.cs b/src/Components/Components/src/Routing/Router.cs index fa4f421ab291d23303a0f5c7ab26a95ea7a06908..7d6b778d291f977dbb01d34d6f1864e74d1bf62f 100644 --- a/src/Components/Components/src/Routing/Router.cs +++ b/src/Components/Components/src/Routing/Router.cs @@ -4,6 +4,7 @@ #nullable disable warnings using System; +using System.Runtime.ExceptionServices; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; @@ -72,7 +73,7 @@ namespace Microsoft.AspNetCore.Components.Routing /// <summary> /// Gets or sets a handler that should be called before navigating to a new page. /// </summary> - [Parameter] public EventCallback<NavigationContext> OnNavigateAsync { get; set; } + [Parameter] public Func<NavigationContext, Task> OnNavigateAsync { get; set; } private RouteTable Routes { get; set; } @@ -194,51 +195,58 @@ namespace Microsoft.AspNetCore.Components.Routing private async ValueTask<bool> RunOnNavigateAsync(string path, Task previousOnNavigate) { - // If this router instance does not provide an OnNavigateAsync parameter - // then we render the component associated with the route as per usual. - if (!OnNavigateAsync.HasDelegate) + if (OnNavigateAsync == null) { return true; } - // If we've already invoked a task and stored its CTS, then - // cancel that existing CTS. + // Cancel the CTS instead of disposing it, since disposing does not + // actually cancel and can cause unintended Object Disposed Exceptions. + // This effectivelly cancels the previously running task and completes it. _onNavigateCts?.Cancel(); - // Then make sure that the task has been completed cancelled or - // completed before continuing with the execution of this current task. + // Then make sure that the task has been completely cancelled or completed + // before starting the next one. This avoid race conditions where the cancellation + // for the previous task was set but not fully completed by the time we get to this + // invocation. await previousOnNavigate; - // Create a new cancellation token source for this instance _onNavigateCts = new CancellationTokenSource(); var navigateContext = new NavigationContext(path, _onNavigateCts.Token); - // Create a cancellation task based on the cancellation token - // associated with the current running task. - var cancellationTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - navigateContext.CancellationToken.Register(state => - ((TaskCompletionSource)state).SetResult(), cancellationTcs); - - var task = OnNavigateAsync.InvokeAsync(navigateContext); - - // If the user provided a Navigating render fragment, then show it. - if (Navigating != null && task.Status != TaskStatus.RanToCompletion) + try + { + if (Navigating != null) + { + _renderHandle.Render(Navigating); + } + await OnNavigateAsync(navigateContext); + return true; + } + catch (OperationCanceledException e) + { + if (e.CancellationToken != navigateContext.CancellationToken) + { + var rethrownException = new InvalidOperationException("OnNavigateAsync can only be cancelled via NavigateContext.CancellationToken.", e); + _renderHandle.Render(builder => ExceptionDispatchInfo.Capture(rethrownException).Throw()); + return false; + } + } + catch (Exception e) { - _renderHandle.Render(Navigating); + _renderHandle.Render(builder => ExceptionDispatchInfo.Capture(e).Throw()); + return false; } - var completedTask = await Task.WhenAny(task, cancellationTcs.Task); - return task == completedTask; + return false; } internal async Task RunOnNavigateWithRefreshAsync(string path, bool isNavigationIntercepted) { // We cache the Task representing the previously invoked RunOnNavigateWithRefreshAsync - // that is stored + // that is stored. Then we create a new one that represents our current invocation and store it + // globally for the next invocation. This allows us to check inside `RunOnNavigateAsync` if the + // previous OnNavigateAsync task has fully completed before starting the next one. var previousTask = _previousOnNavigateTask; - // Then we create a new one that represents our current invocation and store it - // globally for the next invocation. Note to the developer, if the WASM runtime - // support multi-threading then we'll need to implement the appropriate locks - // here to ensure that the cached previous task is overwritten incorrectly. var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); _previousOnNavigateTask = tcs.Task; try diff --git a/src/Components/Components/test/Routing/RouterTest.cs b/src/Components/Components/test/Routing/RouterTest.cs index 7e8a78b382476070a37350e6cfc0c225e65d38b7..62569d5c569c5334e3f4d0b04fe9b334ed3111a2 100644 --- a/src/Components/Components/test/Routing/RouterTest.cs +++ b/src/Components/Components/test/Routing/RouterTest.cs @@ -2,103 +2,253 @@ // 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.Linq; using System.Reflection; using System.Threading; using System.Threading.Tasks; -using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Routing; using Microsoft.AspNetCore.Components.Test.Helpers; -using Microsoft.Extensions.DependencyModel; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using Moq; using Xunit; +using Microsoft.AspNetCore.Components; namespace Microsoft.AspNetCore.Components.Test.Routing { public class RouterTest { + private readonly Router _router; + private readonly TestRenderer _renderer; + + public RouterTest() + { + var services = new ServiceCollection(); + services.AddSingleton<ILoggerFactory>(NullLoggerFactory.Instance); + services.AddSingleton<NavigationManager, TestNavigationManager>(); + services.AddSingleton<INavigationInterception, TestNavigationInterception>(); + var serviceProvider = services.BuildServiceProvider(); + + _renderer = new TestRenderer(serviceProvider); + _renderer.ShouldHandleExceptions = true; + _router = (Router)_renderer.InstantiateComponent<Router>(); + _router.AppAssembly = Assembly.GetExecutingAssembly(); + _router.Found = routeData => (builder) => builder.AddContent(0, "Rendering route..."); + _renderer.AssignRootComponentId(_router); + } + [Fact] public async Task CanRunOnNavigateAsync() { // Arrange - var router = CreateMockRouter(); var called = false; async Task OnNavigateAsync(NavigationContext args) { await Task.CompletedTask; called = true; } - router.Object.OnNavigateAsync = new EventCallbackFactory().Create<NavigationContext>(router, OnNavigateAsync); + _router.OnNavigateAsync = OnNavigateAsync; // Act - await router.Object.RunOnNavigateWithRefreshAsync("http://example.com/jan", false); + await _renderer.Dispatcher.InvokeAsync(() => _router.RunOnNavigateWithRefreshAsync("http://example.com/jan", false)); // Assert Assert.True(called); } [Fact] - public async Task CanCancelPreviousOnNavigateAsync() + public async Task CanHandleSingleFailedOnNavigateAsync() + { + // Arrange + var called = false; + async Task OnNavigateAsync(NavigationContext args) + { + called = true; + await Task.CompletedTask; + throw new Exception("This is an uncaught exception."); + } + _router.OnNavigateAsync = OnNavigateAsync; + + // Act + await _renderer.Dispatcher.InvokeAsync(() => _router.RunOnNavigateWithRefreshAsync("http://example.com/jan", false)); + + // Assert + Assert.True(called); + Assert.Single(_renderer.HandledExceptions); + var unhandledException = _renderer.HandledExceptions[0]; + Assert.Equal("This is an uncaught exception.", unhandledException.Message); + } + + [Fact] + public async Task CanceledFailedOnNavigateAsyncDoesNothing() + { + // Arrange + var onNavigateInvoked = 0; + async Task OnNavigateAsync(NavigationContext args) + { + onNavigateInvoked += 1; + if (args.Path.EndsWith("jan")) + { + await Task.Delay(Timeout.Infinite, args.CancellationToken); + throw new Exception("This is an uncaught exception."); + } + } + var refreshCalled = false; + _renderer.OnUpdateDisplay = (renderBatch) => + { + if (!refreshCalled) + { + refreshCalled = true; + return; + } + Assert.True(false, "OnUpdateDisplay called more than once."); + }; + _router.OnNavigateAsync = OnNavigateAsync; + + // Act + var janTask = _renderer.Dispatcher.InvokeAsync(() => _router.RunOnNavigateWithRefreshAsync("http://example.com/jan", false)); + var febTask = _renderer.Dispatcher.InvokeAsync(() => _router.RunOnNavigateWithRefreshAsync("http://example.com/feb", false)); + + await janTask; + await febTask; + + // Assert that we render the second route component and don't throw an exception + Assert.Empty(_renderer.HandledExceptions); + Assert.Equal(2, onNavigateInvoked); + } + + [Fact] + public async Task CanHandleSingleCancelledOnNavigateAsync() + { + // Arrange + async Task OnNavigateAsync(NavigationContext args) + { + var tcs = new TaskCompletionSource<int>(); + tcs.TrySetCanceled(); + await tcs.Task; + } + _renderer.OnUpdateDisplay = (renderBatch) => Assert.True(false, "OnUpdateDisplay called more than once."); + _router.OnNavigateAsync = OnNavigateAsync; + + // Act + await _renderer.Dispatcher.InvokeAsync(() => _router.RunOnNavigateWithRefreshAsync("http://example.com/jan", false)); + + // Assert + Assert.Single(_renderer.HandledExceptions); + var unhandledException = _renderer.HandledExceptions[0]; + Assert.Equal("OnNavigateAsync can only be cancelled via NavigateContext.CancellationToken.", unhandledException.Message); + } + + [Fact] + public async Task AlreadyCanceledOnNavigateAsyncDoesNothing() + { + // Arrange + var triggerCancel = new TaskCompletionSource(); + async Task OnNavigateAsync(NavigationContext args) + { + if (args.Path.EndsWith("jan")) + { + var tcs = new TaskCompletionSource(); + await triggerCancel.Task; + tcs.TrySetCanceled(); + await tcs.Task; + } + } + var refreshCalled = false; + _renderer.OnUpdateDisplay = (renderBatch) => + { + if (!refreshCalled) + { + Assert.True(true); + return; + } + Assert.True(false, "OnUpdateDisplay called more than once."); + }; + _router.OnNavigateAsync = OnNavigateAsync; + + // Act (start the operations then await them) + var jan = _renderer.Dispatcher.InvokeAsync(() => _router.RunOnNavigateWithRefreshAsync("http://example.com/jan", false)); + var feb = _renderer.Dispatcher.InvokeAsync(() => _router.RunOnNavigateWithRefreshAsync("http://example.com/feb", false)); + triggerCancel.TrySetResult(); + + await jan; + await feb; + } + + [Fact] + public void CanCancelPreviousOnNavigateAsync() { // Arrange - var router = CreateMockRouter(); var cancelled = ""; async Task OnNavigateAsync(NavigationContext args) { await Task.CompletedTask; args.CancellationToken.Register(() => cancelled = args.Path); }; - router.Object.OnNavigateAsync = new EventCallbackFactory().Create<NavigationContext>(router, OnNavigateAsync); + _router.OnNavigateAsync = OnNavigateAsync; // Act - await router.Object.RunOnNavigateWithRefreshAsync("jan", false); - await router.Object.RunOnNavigateWithRefreshAsync("feb", false); + _ = _router.RunOnNavigateWithRefreshAsync("jan", false); + _ = _router.RunOnNavigateWithRefreshAsync("feb", false); // Assert var expected = "jan"; - Assert.Equal(cancelled, expected); + Assert.Equal(expected, cancelled); } [Fact] public async Task RefreshesOnceOnCancelledOnNavigateAsync() { // Arrange - var router = CreateMockRouter(); async Task OnNavigateAsync(NavigationContext args) { if (args.Path.EndsWith("jan")) { - await Task.Delay(Timeout.Infinite); + await Task.Delay(Timeout.Infinite, args.CancellationToken); + } + }; + var refreshCalled = false; + _renderer.OnUpdateDisplay = (renderBatch) => + { + if (!refreshCalled) + { + Assert.True(true); + return; } + Assert.True(false, "OnUpdateDisplay called more than once."); }; - router.Object.OnNavigateAsync = new EventCallbackFactory().Create<NavigationContext>(router, OnNavigateAsync); + _router.OnNavigateAsync = OnNavigateAsync; // Act - var janTask = router.Object.RunOnNavigateWithRefreshAsync("jan", false); - var febTask = router.Object.RunOnNavigateWithRefreshAsync("feb", false); + var jan = _renderer.Dispatcher.InvokeAsync(() => _router.RunOnNavigateWithRefreshAsync("http://example.com/jan", false)); + var feb = _renderer.Dispatcher.InvokeAsync(() => _router.RunOnNavigateWithRefreshAsync("http://example.com/feb", false)); - var janTaskException = await Record.ExceptionAsync(() => janTask); - var febTaskException = await Record.ExceptionAsync(() => febTask); - - // Assert neither execution threw an exception - Assert.Null(janTaskException); - Assert.Null(febTaskException); - // Assert refresh should've only been called once for the second route - router.Verify(x => x.Refresh(false), Times.Once()); + await jan; + await feb; } - private Mock<Router> CreateMockRouter() + internal class TestNavigationManager : NavigationManager { - var router = new Mock<Router>() { CallBase = true }; - router.Setup(x => x.Refresh(It.IsAny<bool>())).Verifiable(); - return router; + public TestNavigationManager() => + Initialize("https://www.example.com/subdir/", "https://www.example.com/subdir/jan"); + + protected override void NavigateToCore(string uri, bool forceLoad) => throw new NotImplementedException(); } - [Route("jan")] - private class JanComponent : ComponentBase { } + internal sealed class TestNavigationInterception : INavigationInterception + { + public static readonly TestNavigationInterception Instance = new TestNavigationInterception(); + + public Task EnableNavigationInterceptionAsync() + { + return Task.CompletedTask; + } + } [Route("feb")] - private class FebComponent : ComponentBase { } + public class FebComponent : ComponentBase { } + + [Route("jan")] + public class JanComponent : ComponentBase { } } } diff --git a/src/Components/ComponentsNoDeps.slnf b/src/Components/ComponentsNoDeps.slnf index 6dbf031fd31fcfddd2fc877481aafda6cc5127af..dd08970d02a727c53d7f756a5ec88e01ab0a0ab1 100644 --- a/src/Components/ComponentsNoDeps.slnf +++ b/src/Components/ComponentsNoDeps.slnf @@ -31,6 +31,10 @@ "src\\Components\\WebAssembly\\testassets\\HostedInAspNet.Client\\HostedInAspNet.Client.csproj", "src\\Components\\WebAssembly\\testassets\\HostedInAspNet.Server\\HostedInAspNet.Server.csproj", "src\\Components\\WebAssembly\\testassets\\StandaloneApp\\StandaloneApp.csproj", + "src\\Components\\WebAssembly\\Sdk\\src\\Microsoft.NET.Sdk.BlazorWebAssembly.csproj", + "src\\Components\\WebAssembly\\Sdk\\test\\Microsoft.NET.Sdk.BlazorWebAssembly.Tests.csproj", + "src\\Components\\WebAssembly\\Sdk\\tools\\Microsoft.NET.Sdk.BlazorWebAssembly.Tools.csproj", + "src\\Components\\WebAssembly\\Sdk\\integrationtests\\Microsoft.NET.Sdk.BlazorWebAssembly.IntegrationTests.csproj", "src\\Components\\Web\\src\\Microsoft.AspNetCore.Components.Web.csproj", "src\\Components\\Web\\test\\Microsoft.AspNetCore.Components.Web.Tests.csproj", "src\\Components\\benchmarkapps\\Wasm.Performance\\Driver\\Wasm.Performance.Driver.csproj", diff --git a/src/Components/Directory.Build.props b/src/Components/Directory.Build.props index 1704a1b070a8e260ab73933c7bb3313fc0db6d96..b1ea764edd5c572387001f038db379ca5c942b6b 100644 --- a/src/Components/Directory.Build.props +++ b/src/Components/Directory.Build.props @@ -1,14 +1,6 @@ <Project> <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory)..\, Directory.Build.props))\Directory.Build.props" /> - <PropertyGroup> - <!-- Workaround for https://github.com/dotnet/aspnetcore/issues/5486 which requires the bin and obj directory be in the project directory --> - <BaseIntermediateOutputPath /> - <IntermediateOutputPath /> - <BaseOutputPath /> - <OutputPath /> - </PropertyGroup> - <PropertyGroup> <EnableTypeScriptNuGetTarget>true</EnableTypeScriptNuGetTarget> </PropertyGroup> diff --git a/src/Components/Directory.Build.targets b/src/Components/Directory.Build.targets index 69204d61a66078921e771e76d62ebf2f27f7fee1..f3143253af19347287e8ea350f54357669e55456 100644 --- a/src/Components/Directory.Build.targets +++ b/src/Components/Directory.Build.targets @@ -3,7 +3,7 @@ <BlazorWebAssemblyJSPath>$(RepoRoot)src\Components\Web.JS\dist\$(Configuration)\blazor.webassembly.js</BlazorWebAssemblyJSPath> <BlazorWebAssemblyJSMapPath>$(BlazorWebAssemblyJSPath).map</BlazorWebAssemblyJSMapPath> - <_BlazorDevServerPath>$(RepoRoot)src/Components/WebAssembly/DevServer/src/bin/$(Configuration)/$(DefaultNetCoreTargetFramework)/blazor-devserver.dll</_BlazorDevServerPath> + <_BlazorDevServerPath>$(ArtifactsDir)/bin/Microsoft.AspNetCore.Components.WebAssembly.DevServer/$(Configuration)/$(DefaultNetCoreTargetFramework)/blazor-devserver.dll</_BlazorDevServerPath> <RunCommand>dotnet</RunCommand> <RunArguments>exec "$(_BlazorDevServerPath)" serve --applicationpath "$(TargetPath)" $(AdditionalRunArguments)</RunArguments> </PropertyGroup> diff --git a/src/Components/WebAssembly/Sdk/integrationtests/Microsoft.NET.Sdk.BlazorWebAssembly.IntegrationTests.csproj b/src/Components/WebAssembly/Sdk/integrationtests/Microsoft.NET.Sdk.BlazorWebAssembly.IntegrationTests.csproj new file mode 100644 index 0000000000000000000000000000000000000000..4255ce7c73eb80b65e6b4639cbb4c4e899bef536 --- /dev/null +++ b/src/Components/WebAssembly/Sdk/integrationtests/Microsoft.NET.Sdk.BlazorWebAssembly.IntegrationTests.csproj @@ -0,0 +1,78 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <!-- + There's not much value in multi-targeting here, this doesn't run much .NET code, it tests MSBuild. + + This is also a partial workaround for https://github.com/Microsoft/msbuild/issues/2661 - this project + has netcoreapp dependencies that need to be built first. + --> + <TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework> + <PreserveCompilationContext>true</PreserveCompilationContext> + + <!-- Tests do not work on Helix yet --> + <BuildHelixPayload>false</BuildHelixPayload> + <TestAppsRoot>$(MSBuildProjectDirectory)\..\testassets\</TestAppsRoot> + </PropertyGroup> + + <Import Project="$(SharedSourceRoot)MSBuild.Testing\MSBuild.Testing.targets" /> + + <ItemGroup> + <None Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" /> + </ItemGroup> + + <ItemGroup> + <Reference Include="Microsoft.Build.Utilities.Core" /> + <Reference Include="Microsoft.Extensions.DependencyModel" /> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="$(RepoRoot)src\Razor\test\Microsoft.AspNetCore.Razor.Test.MvcShim.ClassLib\Microsoft.AspNetCore.Razor.Test.MvcShim.ClassLib.csproj" /> + + <ProjectReference Include="..\src\Microsoft.NET.Sdk.BlazorWebAssembly.csproj"> + <ReferenceOutputAssembly>false</ReferenceOutputAssembly> + <SkipGetTargetFrameworkProperties>true</SkipGetTargetFrameworkProperties> + </ProjectReference> + + <ProjectReference Include="$(RepoRoot)src\Razor\Microsoft.NET.Sdk.Razor\src\Microsoft.NET.Sdk.Razor.csproj"> + <ReferenceOutputAssembly>false</ReferenceOutputAssembly> + <SkipGetTargetFrameworkProperties>true</SkipGetTargetFrameworkProperties> + </ProjectReference> + </ItemGroup> + + <ItemGroup> + <Compile Include="..\src\BootJsonData.cs" LinkBase="Wasm" /> + <Compile Include="..\src\AssetsManifestFile.cs" LinkBase="Wasm" /> + <Compile Include="$(SharedSourceRoot)CommandLineUtils\**\*.cs" /> + </ItemGroup> + + <Target Name="GenerateTestData" BeforeTargets="GetAssemblyAttributes"> + <Exec Condition="'$(OS)' == 'Windows_NT'" Command=""$(NuGetPackageRoot)vswhere\$(VSWhereVersion)\tools\vswhere.exe" -latest -prerelease -property installationPath -requires Microsoft.Component.MSBuild" ConsoleToMsBuild="true" StandardErrorImportance="high"> + <Output TaskParameter="ConsoleOutput" PropertyName="_VSInstallDir" /> + </Exec> + <Error Condition="'$(OS)' == 'Windows_NT' and '$(_VSInstallDir)'=='' and '$(Test)' == 'true'" Text="Visual Studio not found on Windows." /> + + <PropertyGroup> + <_DesktopMSBuildPath Condition="'$(OS)' == 'Windows_NT' and Exists('$(_VSInstallDir)\MSBuild\Current\Bin\msbuild.exe')">$(_VSInstallDir)\MSBuild\Current\Bin\msbuild.exe</_DesktopMSBuildPath> + <_DesktopMSBuildPath Condition="'$(OS)' == 'Windows_NT' and Exists('$(_VSInstallDir)\MSBuild\15.0\Bin\msbuild.exe')">$(_VSInstallDir)\MSBuild\15.0\Bin\msbuild.exe</_DesktopMSBuildPath> + </PropertyGroup> + + <Error Condition="'$(OS)' == 'Windows_NT' and '$(_DesktopMSBuildPath)'=='' and '$(Test)' == 'true'" Text="MSBuild.exe not found on Windows." /> + + <ItemGroup> + <AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute"> + <_Parameter1>DesktopMSBuildPath</_Parameter1> + <_Parameter2>$(_DesktopMSBuildPath)</_Parameter2> + </AssemblyAttribute> + </ItemGroup> + </Target> + + <Target Name="RestoreTestProjects" BeforeTargets="Restore;Build" Condition="'$(DotNetBuildFromSource)' != 'true'"> + <MSBuild Projects="..\testassets\RestoreBlazorWasmTestProjects\RestoreBlazorWasmTestProjects.csproj" Targets="Restore" Properties="MicrosoftNetCompilersToolsetPackageVersion=$(MicrosoftNetCompilersToolsetPackageVersion);RepoRoot=$(RepoRoot)" /> + </Target> + + <Target Name="EnsureLogFolder" AfterTargets="Build"> + <MakeDir Directories="$([MSBuild]::NormalizeDirectory('$(ArtifactsDir)', 'log', '$(_BuildConfig)'))" /> + </Target> + +</Project> diff --git a/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/Wasm/ServiceWorkerAssert.cs b/src/Components/WebAssembly/Sdk/integrationtests/ServiceWorkerAssert.cs similarity index 98% rename from src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/Wasm/ServiceWorkerAssert.cs rename to src/Components/WebAssembly/Sdk/integrationtests/ServiceWorkerAssert.cs index 7585b5b76ca6f05731fb7e2f104894cfde1ffcc9..8597d104805ae0b4c4f1bf8df4d3c10721f99ea1 100644 --- a/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/Wasm/ServiceWorkerAssert.cs +++ b/src/Components/WebAssembly/Sdk/integrationtests/ServiceWorkerAssert.cs @@ -6,7 +6,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.Json; -using Microsoft.AspNetCore.Razor.Tasks; +using Microsoft.NET.Sdk.BlazorWebAssembly; namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests { diff --git a/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/Wasm/WasmBuildIncrementalismTest.cs b/src/Components/WebAssembly/Sdk/integrationtests/WasmBuildIncrementalismTest.cs similarity index 99% rename from src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/Wasm/WasmBuildIncrementalismTest.cs rename to src/Components/WebAssembly/Sdk/integrationtests/WasmBuildIncrementalismTest.cs index f91dd03863e43c087e7d537b105febeda3a4779b..218567fcc543dcc3d89556699922174ab937fb90 100644 --- a/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/Wasm/WasmBuildIncrementalismTest.cs +++ b/src/Components/WebAssembly/Sdk/integrationtests/WasmBuildIncrementalismTest.cs @@ -4,7 +4,7 @@ using System.IO; using System.Text.Json; using System.Threading.Tasks; -using Microsoft.AspNetCore.Razor.Tasks; +using Microsoft.NET.Sdk.BlazorWebAssembly; using Xunit; namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests diff --git a/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/Wasm/WasmBuildIntegrationTest.cs b/src/Components/WebAssembly/Sdk/integrationtests/WasmBuildIntegrationTest.cs similarity index 99% rename from src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/Wasm/WasmBuildIntegrationTest.cs rename to src/Components/WebAssembly/Sdk/integrationtests/WasmBuildIntegrationTest.cs index 12fbbd038048a724a06f7c451bdb8d546888f48c..c521c6be7e9a104f2b92bc1d7f3ddc11ea2e5985 100644 --- a/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/Wasm/WasmBuildIntegrationTest.cs +++ b/src/Components/WebAssembly/Sdk/integrationtests/WasmBuildIntegrationTest.cs @@ -4,7 +4,7 @@ using System.IO; using System.Text.Json; using System.Threading.Tasks; -using Microsoft.AspNetCore.Razor.Tasks; +using Microsoft.NET.Sdk.BlazorWebAssembly; using Microsoft.AspNetCore.Testing; using Xunit; diff --git a/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/Wasm/WasmBuildLazyLoadTest.cs b/src/Components/WebAssembly/Sdk/integrationtests/WasmBuildLazyLoadTest.cs similarity index 99% rename from src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/Wasm/WasmBuildLazyLoadTest.cs rename to src/Components/WebAssembly/Sdk/integrationtests/WasmBuildLazyLoadTest.cs index e8f1a02d40f4f1009069b90443422cea30ed8277..424caeda2d8088c88a4fd48eed1d119b4991bd40 100644 --- a/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/Wasm/WasmBuildLazyLoadTest.cs +++ b/src/Components/WebAssembly/Sdk/integrationtests/WasmBuildLazyLoadTest.cs @@ -4,7 +4,7 @@ using System.IO; using System.Text.Json; using System.Threading.Tasks; -using Microsoft.AspNetCore.Razor.Tasks; +using Microsoft.NET.Sdk.BlazorWebAssembly; using Xunit; namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests diff --git a/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/Wasm/WasmCompressionTests.cs b/src/Components/WebAssembly/Sdk/integrationtests/WasmCompressionTests.cs similarity index 100% rename from src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/Wasm/WasmCompressionTests.cs rename to src/Components/WebAssembly/Sdk/integrationtests/WasmCompressionTests.cs diff --git a/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/Wasm/WasmPublishIntegrationTest.cs b/src/Components/WebAssembly/Sdk/integrationtests/WasmPublishIntegrationTest.cs similarity index 98% rename from src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/Wasm/WasmPublishIntegrationTest.cs rename to src/Components/WebAssembly/Sdk/integrationtests/WasmPublishIntegrationTest.cs index 45c878ea5aa93d389bce6b4f5d929e84123a9bd7..1108d0bff94e2f5675ebf4937934a7e033eb4d9f 100644 --- a/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/Wasm/WasmPublishIntegrationTest.cs +++ b/src/Components/WebAssembly/Sdk/integrationtests/WasmPublishIntegrationTest.cs @@ -5,7 +5,7 @@ using System.IO; using System.IO.Compression; using System.Text.Json; using System.Threading.Tasks; -using Microsoft.AspNetCore.Razor.Tasks; +using Microsoft.NET.Sdk.BlazorWebAssembly; using Microsoft.AspNetCore.Testing; using Xunit; using static Microsoft.AspNetCore.Razor.Design.IntegrationTests.ServiceWorkerAssert; @@ -608,6 +608,17 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests assetsManifestPath: "custom-service-worker-assets.js"); } + [Fact] + public async Task Publish_HostedApp_WithRidSpecifiedInCLI_Works() + { + // Arrange + using var project = ProjectDirectory.Create("blazorhosted-rid", additionalProjects: new[] { "blazorwasm", "razorclasslibrary", }); + project.RuntimeIdentifier = "linux-x64"; + var result = await MSBuildProcessManager.DotnetMSBuild(project, "Publish", "/p:RuntimeIdentifier=linux-x64"); + + AssertRIDPublishOuput(project, result); + } + [Fact] public async Task Publish_HostedApp_WithRid_Works() { @@ -616,6 +627,11 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests project.RuntimeIdentifier = "linux-x64"; var result = await MSBuildProcessManager.DotnetMSBuild(project, "Publish"); + AssertRIDPublishOuput(project, result); + } + + private static void AssertRIDPublishOuput(ProjectDirectory project, MSBuildResult result) + { Assert.BuildPassed(result); var publishDirectory = project.PublishOutputDirectory; diff --git a/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/Wasm/WasmPwaManifestTests.cs b/src/Components/WebAssembly/Sdk/integrationtests/WasmPwaManifestTests.cs similarity index 100% rename from src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/Wasm/WasmPwaManifestTests.cs rename to src/Components/WebAssembly/Sdk/integrationtests/WasmPwaManifestTests.cs diff --git a/src/Components/WebAssembly/Sdk/integrationtests/xunit.runner.json b/src/Components/WebAssembly/Sdk/integrationtests/xunit.runner.json new file mode 100644 index 0000000000000000000000000000000000000000..d00e4ae907442030f89d8b5a4a9a7ab6b5c5ba88 --- /dev/null +++ b/src/Components/WebAssembly/Sdk/integrationtests/xunit.runner.json @@ -0,0 +1,5 @@ +{ + "methodDisplay": "method", + "shadowCopy": false, + "maxParallelThreads": -1 +} \ No newline at end of file diff --git a/src/Components/WebAssembly/Sdk/src/AssetsManifestFile.cs b/src/Components/WebAssembly/Sdk/src/AssetsManifestFile.cs new file mode 100644 index 0000000000000000000000000000000000000000..872b17aae220aaee34715feca27a0eb0d99a8a98 --- /dev/null +++ b/src/Components/WebAssembly/Sdk/src/AssetsManifestFile.cs @@ -0,0 +1,33 @@ +// 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.NET.Sdk.BlazorWebAssembly +{ +#pragma warning disable IDE1006 // Naming Styles + public class AssetsManifestFile + { + /// <summary> + /// Gets or sets a version string. + /// </summary> + public string version { get; set; } + + /// <summary> + /// Gets or sets the assets. Keys are URLs; values are base-64-formatted SHA256 content hashes. + /// </summary> + public AssetsManifestFileEntry[] assets { get; set; } + } + + public class AssetsManifestFileEntry + { + /// <summary> + /// Gets or sets the asset URL. Normally this will be relative to the application's base href. + /// </summary> + public string url { get; set; } + + /// <summary> + /// Gets or sets the file content hash. This should be the base-64-formatted SHA256 value. + /// </summary> + public string hash { get; set; } + } +#pragma warning restore IDE1006 // Naming Styles +} diff --git a/src/Components/WebAssembly/Sdk/src/BlazorReadSatelliteAssemblyFile.cs b/src/Components/WebAssembly/Sdk/src/BlazorReadSatelliteAssemblyFile.cs new file mode 100644 index 0000000000000000000000000000000000000000..f6bf0c88f8d09c6af5c5b82b806f97d03d466e6b --- /dev/null +++ b/src/Components/WebAssembly/Sdk/src/BlazorReadSatelliteAssemblyFile.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.Linq; +using System.Xml.Linq; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +namespace Microsoft.NET.Sdk.BlazorWebAssembly +{ + public class BlazorReadSatelliteAssemblyFile : Task + { + [Output] + public ITaskItem[] SatelliteAssembly { get; set; } + + [Required] + public ITaskItem ReadFile { get; set; } + + public override bool Execute() + { + var document = XDocument.Load(ReadFile.ItemSpec); + SatelliteAssembly = document.Root + .Elements() + .Select(e => + { + // <Assembly Name="..." Culture="..." DestinationSubDirectory="..." /> + + var taskItem = new TaskItem(e.Attribute("Name").Value); + taskItem.SetMetadata("Culture", e.Attribute("Culture").Value); + taskItem.SetMetadata("DestinationSubDirectory", e.Attribute("DestinationSubDirectory").Value); + + return taskItem; + }).ToArray(); + + return true; + } + } +} diff --git a/src/Components/WebAssembly/Sdk/src/BlazorWriteSatelliteAssemblyFile.cs b/src/Components/WebAssembly/Sdk/src/BlazorWriteSatelliteAssemblyFile.cs new file mode 100644 index 0000000000000000000000000000000000000000..92fd8d33e874a0230a79aaa500d1f513966f96c7 --- /dev/null +++ b/src/Components/WebAssembly/Sdk/src/BlazorWriteSatelliteAssemblyFile.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.IO; +using System.Xml; +using System.Xml.Linq; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +namespace Microsoft.NET.Sdk.BlazorWebAssembly +{ + public class BlazorWriteSatelliteAssemblyFile : Task + { + [Required] + public ITaskItem[] SatelliteAssembly { get; set; } + + [Required] + public ITaskItem WriteFile { get; set; } + + public override bool Execute() + { + using var fileStream = File.Create(WriteFile.ItemSpec); + WriteSatelliteAssemblyFile(fileStream); + return true; + } + + internal void WriteSatelliteAssemblyFile(Stream stream) + { + var root = new XElement("SatelliteAssembly"); + + foreach (var item in SatelliteAssembly) + { + // <Assembly Name="..." Culture="..." DestinationSubDirectory="..." /> + + root.Add(new XElement("Assembly", + new XAttribute("Name", item.ItemSpec), + new XAttribute("Culture", item.GetMetadata("Culture")), + new XAttribute("DestinationSubDirectory", item.GetMetadata("DestinationSubDirectory")))); + } + + var xmlWriterSettings = new XmlWriterSettings + { + Indent = true, + OmitXmlDeclaration = true + }; + + using var writer = XmlWriter.Create(stream, xmlWriterSettings); + var xDocument = new XDocument(root); + + xDocument.Save(writer); + } + } +} diff --git a/src/Components/WebAssembly/Sdk/src/BootJsonData.cs b/src/Components/WebAssembly/Sdk/src/BootJsonData.cs new file mode 100644 index 0000000000000000000000000000000000000000..b3a2dbd3884763b94d77faa006b55730594867be --- /dev/null +++ b/src/Components/WebAssembly/Sdk/src/BootJsonData.cs @@ -0,0 +1,85 @@ +// 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.Runtime.Serialization; +using ResourceHashesByNameDictionary = System.Collections.Generic.Dictionary<string, string>; + +namespace Microsoft.NET.Sdk.BlazorWebAssembly +{ +#pragma warning disable IDE1006 // Naming Styles + /// <summary> + /// Defines the structure of a Blazor boot JSON file + /// </summary> + public class BootJsonData + { + /// <summary> + /// Gets the name of the assembly with the application entry point + /// </summary> + public string entryAssembly { get; set; } + + /// <summary> + /// Gets the set of resources needed to boot the application. This includes the transitive + /// closure of .NET assemblies (including the entrypoint assembly), the dotnet.wasm file, + /// and any PDBs to be loaded. + /// + /// Within <see cref="ResourceHashesByNameDictionary"/>, dictionary keys are resource names, + /// and values are SHA-256 hashes formatted in prefixed base-64 style (e.g., 'sha256-abcdefg...') + /// as used for subresource integrity checking. + /// </summary> + public ResourcesData resources { get; set; } = new ResourcesData(); + + /// <summary> + /// Gets a value that determines whether to enable caching of the <see cref="resources"/> + /// inside a CacheStorage instance within the browser. + /// </summary> + public bool cacheBootResources { get; set; } + + /// <summary> + /// Gets a value that determines if this is a debug build. + /// </summary> + public bool debugBuild { get; set; } + + /// <summary> + /// Gets a value that determines if the linker is enabled. + /// </summary> + public bool linkerEnabled { get; set; } + + /// <summary> + /// Config files for the application + /// </summary> + public List<string> config { get; set; } + } + + public class ResourcesData + { + /// <summary> + /// .NET Wasm runtime resources (dotnet.wasm, dotnet.js) etc. + /// </summary> + public ResourceHashesByNameDictionary runtime { get; set; } = new ResourceHashesByNameDictionary(); + + /// <summary> + /// "assembly" (.dll) resources + /// </summary> + public ResourceHashesByNameDictionary assembly { get; set; } = new ResourceHashesByNameDictionary(); + + /// <summary> + /// "debug" (.pdb) resources + /// </summary> + [DataMember(EmitDefaultValue = false)] + public ResourceHashesByNameDictionary pdb { get; set; } + + /// <summary> + /// localization (.satellite resx) resources + /// </summary> + [DataMember(EmitDefaultValue = false)] + public Dictionary<string, ResourceHashesByNameDictionary> satelliteResources { get; set; } + + /// <summary> + /// Assembly (.dll) resources that are loaded lazily during runtime + /// </summary> + [DataMember(EmitDefaultValue = false)] + public ResourceHashesByNameDictionary lazyAssembly { get; set; } + } +#pragma warning restore IDE1006 // Naming Styles +} diff --git a/src/Components/WebAssembly/Sdk/src/BrotliCompress.cs b/src/Components/WebAssembly/Sdk/src/BrotliCompress.cs new file mode 100644 index 0000000000000000000000000000000000000000..89b3004a5edb691befa0ad1443200a877557d015 --- /dev/null +++ b/src/Components/WebAssembly/Sdk/src/BrotliCompress.cs @@ -0,0 +1,125 @@ +// 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.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +namespace Microsoft.NET.Sdk.BlazorWebAssembly +{ + public class BrotliCompress : ToolTask + { + private static readonly char[] InvalidPathChars = Path.GetInvalidFileNameChars(); + private string _dotnetPath; + + [Required] + public ITaskItem[] FilesToCompress { get; set; } + + [Output] + public ITaskItem[] CompressedFiles { get; set; } + + [Required] + public string OutputDirectory { get; set; } + + public string CompressionLevel { get; set; } + + public bool SkipIfOutputIsNewer { get; set; } + + [Required] + public string ToolAssembly { get; set; } + + protected override string ToolName => Path.GetDirectoryName(DotNetPath); + + private string DotNetPath + { + get + { + if (!string.IsNullOrEmpty(_dotnetPath)) + { + return _dotnetPath; + } + + _dotnetPath = Environment.GetEnvironmentVariable("DOTNET_HOST_PATH"); + if (string.IsNullOrEmpty(_dotnetPath)) + { + throw new InvalidOperationException("DOTNET_HOST_PATH is not set"); + } + + return _dotnetPath; + } + } + + protected override string GenerateCommandLineCommands() => ToolAssembly; + + protected override string GenerateResponseFileCommands() + { + var builder = new StringBuilder(); + + + builder.AppendLine("brotli"); + + if (!string.IsNullOrEmpty(CompressionLevel)) + { + builder.AppendLine("-c"); + builder.AppendLine(CompressionLevel); + } + + CompressedFiles = new ITaskItem[FilesToCompress.Length]; + + for (var i = 0; i < FilesToCompress.Length; i++) + { + var input = FilesToCompress[i]; + var inputFullPath = input.GetMetadata("FullPath"); + var relativePath = input.GetMetadata("RelativePath"); + var outputRelativePath = Path.Combine(OutputDirectory, CalculateTargetPath(inputFullPath, ".br")); + var outputFullPath = Path.GetFullPath(outputRelativePath); + + var outputItem = new TaskItem(outputRelativePath); + outputItem.SetMetadata("RelativePath", relativePath + ".br"); + CompressedFiles[i] = outputItem; + + if (SkipIfOutputIsNewer && File.Exists(outputFullPath) && File.GetLastWriteTimeUtc(inputFullPath) < File.GetLastWriteTimeUtc(outputFullPath)) + { + Log.LogMessage(MessageImportance.Low, $"Skipping compression for '{input.ItemSpec}' because '{outputRelativePath}' is newer than '{input.ItemSpec}'."); + continue; + } + + builder.AppendLine("-s"); + builder.AppendLine(inputFullPath); + + builder.AppendLine("-o"); + builder.AppendLine(outputFullPath); + } + + return builder.ToString(); + } + + internal static string CalculateTargetPath(string relativePath, string extension) + { + // RelativePath can be long and if used as-is to write the output, might result in long path issues on Windows. + // Instead we'll calculate a fixed length path by hashing the input file name. This uses SHA1 similar to the Hash task in MSBuild + // since it has no crytographic significance. + using var hash = SHA1.Create(); + var bytes = Encoding.UTF8.GetBytes(relativePath); + var hashString = Convert.ToBase64String(hash.ComputeHash(bytes)); + + var builder = new StringBuilder(); + + for (var i = 0; i < 8; i++) + { + var c = hashString[i]; + builder.Append(InvalidPathChars.Contains(c) ? '+' : c); + } + + builder.Append(extension); + return builder.ToString(); + } + + protected override string GenerateFullPathToTool() => DotNetPath; + } +} diff --git a/src/Components/WebAssembly/Sdk/src/CreateBlazorTrimmerRootDescriptorFile.cs b/src/Components/WebAssembly/Sdk/src/CreateBlazorTrimmerRootDescriptorFile.cs new file mode 100644 index 0000000000000000000000000000000000000000..f14ffad2219ab4c7bd7418cb794c3b98151e9897 --- /dev/null +++ b/src/Components/WebAssembly/Sdk/src/CreateBlazorTrimmerRootDescriptorFile.cs @@ -0,0 +1,79 @@ +// 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.Xml; +using System.Xml.Linq; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +namespace Microsoft.NET.Sdk.BlazorWebAssembly +{ + // Based on https://github.com/mono/linker/blob/3b329b9481e300bcf4fb88a2eebf8cb5ef8b323b/src/ILLink.Tasks/CreateRootDescriptorFile.cs + public class CreateBlazorTrimmerRootDescriptorFile : Task + { + [Required] + public ITaskItem[] Assemblies { get; set; } + + [Required] + public ITaskItem TrimmerFile { get; set; } + + public override bool Execute() + { + var rootDescriptor = CreateRootDescriptorContents(); + if (File.Exists(TrimmerFile.ItemSpec)) + { + var existing = File.ReadAllText(TrimmerFile.ItemSpec); + + if (string.Equals(rootDescriptor, existing, StringComparison.Ordinal)) + { + Log.LogMessage(MessageImportance.Low, "Skipping write to file {0} because contents would not change.", TrimmerFile.ItemSpec); + // Avoid writing if the file contents are identical. This is required for build incrementalism. + return !Log.HasLoggedErrors; + } + } + + File.WriteAllText(TrimmerFile.ItemSpec, rootDescriptor); + return !Log.HasLoggedErrors; + } + + internal string CreateRootDescriptorContents() + { + var roots = new XElement("linker"); + foreach (var assembly in Assemblies.OrderBy(a => a.ItemSpec)) + { + // NOTE: Descriptor files don't include the file extension + // in the assemblyName. + var assemblyName = assembly.GetMetadata("FileName"); + var typePreserved = assembly.GetMetadata("Preserve"); + var typeRequired = assembly.GetMetadata("Required"); + + var attributes = new List<XAttribute> + { + new XAttribute("fullname", "*"), + new XAttribute("required", typeRequired), + }; + + if (!string.IsNullOrEmpty(typePreserved)) + { + attributes.Add(new XAttribute("preserve", typePreserved)); + } + + roots.Add(new XElement("assembly", + new XAttribute("fullname", assemblyName), + new XElement("type", attributes))); + } + + var xmlWriterSettings = new XmlWriterSettings + { + Indent = true, + OmitXmlDeclaration = true + }; + + return new XDocument(roots).Root.ToString(); + } + } +} diff --git a/src/Components/WebAssembly/Sdk/src/GZipCompress.cs b/src/Components/WebAssembly/Sdk/src/GZipCompress.cs new file mode 100644 index 0000000000000000000000000000000000000000..4b8a0c542a1b9a467c6fdec48651794d59f981cb --- /dev/null +++ b/src/Components/WebAssembly/Sdk/src/GZipCompress.cs @@ -0,0 +1,70 @@ +// 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.IO.Compression; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +namespace Microsoft.NET.Sdk.BlazorWebAssembly +{ + public class GZipCompress : Task + { + [Required] + public ITaskItem[] FilesToCompress { get; set; } + + [Output] + public ITaskItem[] CompressedFiles { get; set; } + + [Required] + public string OutputDirectory { get; set; } + + public override bool Execute() + { + CompressedFiles = new ITaskItem[FilesToCompress.Length]; + + Directory.CreateDirectory(OutputDirectory); + + System.Threading.Tasks.Parallel.For(0, FilesToCompress.Length, i => + { + var file = FilesToCompress[i]; + var inputPath = file.ItemSpec; + var relativePath = file.GetMetadata("RelativePath"); + var outputRelativePath = Path.Combine( + OutputDirectory, + BrotliCompress.CalculateTargetPath(relativePath, ".gz")); + + var outputItem = new TaskItem(outputRelativePath); + outputItem.SetMetadata("RelativePath", relativePath + ".gz"); + CompressedFiles[i] = outputItem; + + if (File.Exists(outputRelativePath) && File.GetLastWriteTimeUtc(inputPath) < File.GetLastWriteTimeUtc(outputRelativePath)) + { + // Incrementalism. If input source doesn't exist or it exists and is not newer than the expected output, do nothing. + Log.LogMessage(MessageImportance.Low, $"Skipping '{inputPath}' because '{outputRelativePath}' is newer than '{inputPath}'."); + return; + } + + try + { + using var sourceStream = File.OpenRead(inputPath); + using var fileStream = File.Create(outputRelativePath); + using var stream = new GZipStream(fileStream, CompressionLevel.Optimal); + + sourceStream.CopyTo(stream); + } + catch (Exception e) + { + Log.LogErrorFromException(e); + return; + } + }); + + return !Log.HasLoggedErrors; + } + } +} diff --git a/src/Components/WebAssembly/Sdk/src/GenerateBlazorWebAssemblyBootJson.cs b/src/Components/WebAssembly/Sdk/src/GenerateBlazorWebAssemblyBootJson.cs new file mode 100644 index 0000000000000000000000000000000000000000..81cb6ecb96dbf7c7a567978018fcdae779f9e515 --- /dev/null +++ b/src/Components/WebAssembly/Sdk/src/GenerateBlazorWebAssemblyBootJson.cs @@ -0,0 +1,155 @@ +// 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.Runtime.Serialization.Json; +using System.Text; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using ResourceHashesByNameDictionary = System.Collections.Generic.Dictionary<string, string>; + +namespace Microsoft.NET.Sdk.BlazorWebAssembly +{ + public class GenerateBlazorWebAssemblyBootJson : Task + { + [Required] + public string AssemblyPath { get; set; } + + [Required] + public ITaskItem[] Resources { get; set; } + + [Required] + public bool DebugBuild { get; set; } + + [Required] + public bool LinkerEnabled { get; set; } + + [Required] + public bool CacheBootResources { get; set; } + + public ITaskItem[] ConfigurationFiles { get; set; } + + [Required] + public string OutputPath { get; set; } + + public ITaskItem[] LazyLoadedAssemblies { get; set; } + + public override bool Execute() + { + using var fileStream = File.Create(OutputPath); + var entryAssemblyName = AssemblyName.GetAssemblyName(AssemblyPath).Name; + + try + { + WriteBootJson(fileStream, entryAssemblyName); + } + catch (Exception ex) + { + Log.LogErrorFromException(ex); + } + + return !Log.HasLoggedErrors; + } + + // Internal for tests + public void WriteBootJson(Stream output, string entryAssemblyName) + { + var result = new BootJsonData + { + entryAssembly = entryAssemblyName, + cacheBootResources = CacheBootResources, + debugBuild = DebugBuild, + linkerEnabled = LinkerEnabled, + resources = new ResourcesData(), + config = new List<string>(), + }; + + // Build a two-level dictionary of the form: + // - assembly: + // - UriPath (e.g., "System.Text.Json.dll") + // - ContentHash (e.g., "4548fa2e9cf52986") + // - runtime: + // - UriPath (e.g., "dotnet.js") + // - ContentHash (e.g., "3448f339acf512448") + if (Resources != null) + { + var resourceData = result.resources; + foreach (var resource in Resources) + { + ResourceHashesByNameDictionary resourceList; + + var fileName = resource.GetMetadata("FileName"); + var extension = resource.GetMetadata("Extension"); + var resourceCulture = resource.GetMetadata("Culture"); + var assetType = resource.GetMetadata("AssetType"); + var resourceName = $"{fileName}{extension}"; + + if (IsLazyLoadedAssembly(fileName)) + { + resourceData.lazyAssembly ??= new ResourceHashesByNameDictionary(); + resourceList = resourceData.lazyAssembly; + } + else if (!string.IsNullOrEmpty(resourceCulture)) + { + resourceData.satelliteResources ??= new Dictionary<string, ResourceHashesByNameDictionary>(StringComparer.OrdinalIgnoreCase); + resourceName = resourceCulture + "/" + resourceName; + + if (!resourceData.satelliteResources.TryGetValue(resourceCulture, out resourceList)) + { + resourceList = new ResourceHashesByNameDictionary(); + resourceData.satelliteResources.Add(resourceCulture, resourceList); + } + } + else if (string.Equals(extension, ".pdb", StringComparison.OrdinalIgnoreCase)) + { + resourceData.pdb ??= new ResourceHashesByNameDictionary(); + resourceList = resourceData.pdb; + } + else if (string.Equals(extension, ".dll", StringComparison.OrdinalIgnoreCase)) + { + resourceList = resourceData.assembly; + } + else if (string.Equals(assetType, "native", StringComparison.OrdinalIgnoreCase)) + { + resourceList = resourceData.runtime; + } + else + { + // This should include items such as XML doc files, which do not need to be recorded in the manifest. + continue; + } + + if (!resourceList.ContainsKey(resourceName)) + { + resourceList.Add(resourceName, $"sha256-{resource.GetMetadata("FileHash")}"); + } + } + } + + if (ConfigurationFiles != null) + { + foreach (var configFile in ConfigurationFiles) + { + result.config.Add(Path.GetFileName(configFile.ItemSpec)); + } + } + + var serializer = new DataContractJsonSerializer(typeof(BootJsonData), new DataContractJsonSerializerSettings + { + UseSimpleDictionaryFormat = true + }); + + using var writer = JsonReaderWriterFactory.CreateJsonWriter(output, Encoding.UTF8, ownsStream: false, indent: true); + serializer.WriteObject(writer, result); + } + + private bool IsLazyLoadedAssembly(string fileName) + { + return LazyLoadedAssemblies != null && LazyLoadedAssemblies.Any(a => a.ItemSpec == fileName); + } + } +} diff --git a/src/Components/WebAssembly/Sdk/src/GenerateServiceWorkerAssetsManifest.cs b/src/Components/WebAssembly/Sdk/src/GenerateServiceWorkerAssetsManifest.cs new file mode 100644 index 0000000000000000000000000000000000000000..08c75a927af75c98350f9ed65f60d80b35f406cd --- /dev/null +++ b/src/Components/WebAssembly/Sdk/src/GenerateServiceWorkerAssetsManifest.cs @@ -0,0 +1,97 @@ +// 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.Runtime.Serialization.Json; +using System.Security.Cryptography; +using System.Text; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +namespace Microsoft.NET.Sdk.BlazorWebAssembly +{ + public partial class GenerateServiceWorkerAssetsManifest : Task + { + [Required] + public ITaskItem[] Assets { get; set; } + + public string Version { get; set; } + + [Required] + public string OutputPath { get; set; } + + [Output] + public string CalculatedVersion { get; set; } + + public override bool Execute() + { + using var fileStream = File.Create(OutputPath); + CalculatedVersion = GenerateAssetManifest(fileStream); + + return true; + } + + internal string GenerateAssetManifest(Stream stream) + { + var assets = new AssetsManifestFileEntry[Assets.Length]; + System.Threading.Tasks.Parallel.For(0, assets.Length, i => + { + var item = Assets[i]; + var hash = item.GetMetadata("FileHash"); + var url = item.GetMetadata("AssetUrl"); + + if (string.IsNullOrEmpty(hash)) + { + // Some files that are part of the service worker manifest may not have their hashes previously + // calcualted. Calculate them at this time. + using var sha = SHA256.Create(); + using var file = File.OpenRead(item.ItemSpec); + var bytes = sha.ComputeHash(file); + + hash = Convert.ToBase64String(bytes); + } + + assets[i] = new AssetsManifestFileEntry + { + hash = "sha256-" + hash, + url = url, + }; + }); + + var version = Version; + if (string.IsNullOrEmpty(version)) + { + // If a version isn't specified (which is likely the most common case), construct a Version by combining + // the file names + hashes of all the inputs. + + var combinedHash = string.Join( + Environment.NewLine, + assets.OrderBy(f => f.url, StringComparer.Ordinal).Select(f => f.hash)); + + using var sha = SHA256.Create(); + var bytes = sha.ComputeHash(Encoding.UTF8.GetBytes(combinedHash)); + version = Convert.ToBase64String(bytes).Substring(0, 8); + } + + var data = new AssetsManifestFile + { + version = version, + assets = assets, + }; + + using var streamWriter = new StreamWriter(stream, Encoding.UTF8, bufferSize: 50, leaveOpen: true); + streamWriter.Write("self.assetsManifest = "); + streamWriter.Flush(); + + using var jsonWriter = JsonReaderWriterFactory.CreateJsonWriter(stream, Encoding.UTF8, ownsStream: false, indent: true); + new DataContractJsonSerializer(typeof(AssetsManifestFile)).WriteObject(jsonWriter, data); + jsonWriter.Flush(); + + streamWriter.WriteLine(";"); + + return version; + } + } +} diff --git a/src/Components/WebAssembly/Sdk/src/Microsoft.NET.Sdk.BlazorWebAssembly.csproj b/src/Components/WebAssembly/Sdk/src/Microsoft.NET.Sdk.BlazorWebAssembly.csproj new file mode 100644 index 0000000000000000000000000000000000000000..adf7330a3feede765baf2dfe3bd044a0431965f5 --- /dev/null +++ b/src/Components/WebAssembly/Sdk/src/Microsoft.NET.Sdk.BlazorWebAssembly.csproj @@ -0,0 +1,89 @@ +<Project Sdk="Microsoft.NET.Sdk"> + <PropertyGroup> + <Description>MSBuild support for building Blazor WebAssembly apps.</Description> + <TargetFrameworks>$(DefaultNetCoreTargetFramework);net46</TargetFrameworks> + + <TargetName>Microsoft.NET.Sdk.BlazorWebAssembly.Tasks</TargetName> + <NuspecFile>$(MSBuildProjectName).nuspec</NuspecFile> + <Serviceable>true</Serviceable> + <SdkOutputPath>$(ArtifactsBinDir)Microsoft.NET.Sdk.BlazorWebAssembly\$(Configuration)\sdk-output\</SdkOutputPath> + + <!-- Allow assemblies outside of lib in the package --> + <NoWarn>$(NoWarn);NU5100</NoWarn> + <!-- Need to build this project in source build --> + <ExcludeFromSourceBuild>false</ExcludeFromSourceBuild> + </PropertyGroup> + + <ItemGroup> + <Reference Include="Microsoft.Build.Framework" /> + <Reference Include="Microsoft.Build.Utilities.Core" /> + + <ProjectReference + Include="..\tools\Microsoft.NET.Sdk.BlazorWebAssembly.Tools.csproj" + Targets="Publish" + ReferenceOutputAssembly="false" + IsImplicityDefined="false" + SkipGetTargetFrameworkProperties="true" + UndefineProperties="TargetFramework;TargetFrameworks;RuntimeIdentifier;PublishDir" /> + </ItemGroup> + + <ItemGroup> + <Content Include="_._" CopyToOutputDirectory="PreserveNewest" /> + </ItemGroup> + + <Target Name="LayoutDependencies" BeforeTargets="Build" + Condition="'$(IsInnerBuild)' != 'true' AND '$(NoBuild)' != 'true'"> + <!-- Layout tasks, compiler, and extensions in the sdk-output folder. The entire folder structure gets packaged as-is into the SDK --> + + <PropertyGroup Condition="'$(ContinuousIntegrationBuild)' != 'true'"> + <_ContinueOnError>true</_ContinueOnError> + <_Retries>1</_Retries> + </PropertyGroup> + + <PropertyGroup Condition="'$(ContinuousIntegrationBuild)' == 'true'"> + <_ContinueOnError>false</_ContinueOnError> + <_Retries>10</_Retries> + </PropertyGroup> + + <ItemGroup> + <_WebAssemblyToolsOutput Include="$(ArtifactsBinDir)Microsoft.NET.Sdk.BlazorWebAssembly.Tools\$(Configuration)\$(DefaultNetCoreTargetFramework)\publish\Microsoft.*" /> + </ItemGroup> + + <Error + Text="WebAssembly SDK tools outputs were not found in $(ArtifactsBinDir)Microsoft.NET.Sdk.BlazorWebAssembly.Tools\$(Configuration)\$(DefaultNetCoreTargetFramework)\publish" + Condition="'@(_WebAssemblyToolsOutput->Count())' == '0'" /> + + <Copy + SourceFiles="@(_WebAssemblyToolsOutput)" + DestinationFolder="$(SdkOutputPath)tools\$(DefaultNetCoreTargetFramework)\" + SkipUnchangedFiles="true" + Retries="$(_Retries)" + ContinueOnError="$(_ContinueOnError)" /> + + <ItemGroup> + <ProjectOutput Include="$(ArtifactsBinDir)Microsoft.NET.Sdk.BlazorWebAssembly\$(Configuration)\net46*\Microsoft.NET.Sdk.BlazorWebAssembly.*" /> + <ProjectOutput Include="$(ArtifactsBinDir)Microsoft.NET.Sdk.BlazorWebAssembly\$(Configuration)\$(DefaultNetCoreTargetFramework)*\Microsoft.NET.Sdk.BlazorWebAssembly.*" /> + </ItemGroup> + + <Copy SourceFiles="@(ProjectOutput)" DestinationFiles="$(SdkOutputPath)tasks\%(RecursiveDir)%(FileName)%(Extension)" SkipUnchangedFiles="true" Retries="$(_Retries)" ContinueOnError="$(_ContinueOnError)"> + <Output TaskParameter="CopiedFiles" ItemName="FileWrites" /> + </Copy> + + <Message Text="Blazor WebAssembly SDK output -> $(SdkOutputPath)" Importance="High" /> + </Target> + + <Target Name="PopulateNuspec" BeforeTargets="InitializeStandardNuspecProperties" DependsOnTargets="LayoutDependencies"> + <PropertyGroup> + <PackageTags>$(PackageTags.Replace(';',' '))</PackageTags> + </PropertyGroup> + + <ItemGroup> + <NuspecProperty Include="outputPath=$(OutputPath)\sdk-output" /> + </ItemGroup> + </Target> + + <!-- Workarounds to allow publishing to work when the SDK is referenced as a project. --> + <Target Name="GetTargetPath" /> + <Target Name="GetCopyToPublishDirectoryItems" /> + +</Project> diff --git a/src/Components/WebAssembly/Sdk/src/Microsoft.NET.Sdk.BlazorWebAssembly.nuspec b/src/Components/WebAssembly/Sdk/src/Microsoft.NET.Sdk.BlazorWebAssembly.nuspec new file mode 100644 index 0000000000000000000000000000000000000000..6d74a1931cca5bd02c843a03d8876e85f16834ef --- /dev/null +++ b/src/Components/WebAssembly/Sdk/src/Microsoft.NET.Sdk.BlazorWebAssembly.nuspec @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<package xmlns="http://schemas.microsoft.com/packaging/2012/06/nuspec.xsd"> + <metadata> + $CommonMetadataElements$ + <dependencies> + <group targetFramework=".NET5.0" /> + </dependencies> + </metadata> + + <files> + $CommonFileElements$ + <file src="Sdk\*" target="Sdk" /> + <file src="build\**" target="build" /> + <file src="targets\**" target="targets" /> + <file src="_._" target="lib\net5.0\_._" /> + + <file src="$outputPath$\**" target="\" /> + </files> +</package> diff --git a/src/Components/WebAssembly/Sdk/src/Sdk/Sdk.props b/src/Components/WebAssembly/Sdk/src/Sdk/Sdk.props new file mode 100644 index 0000000000000000000000000000000000000000..3f6870441f48028ecae226e762e97d6c88a0d490 --- /dev/null +++ b/src/Components/WebAssembly/Sdk/src/Sdk/Sdk.props @@ -0,0 +1,22 @@ +<!-- +*********************************************************************************************** +Sdk.props + +WARNING: DO NOT MODIFY this file unless you are knowledgeable about MSBuild and have + created a backup copy. Incorrect changes to this file will make it + impossible to load or build your projects from the command-line or the IDE. + +Copyright (c) .NET Foundation. All rights reserved. +*********************************************************************************************** +--> +<Project ToolsVersion="14.0" TreatAsLocalProperty="RuntimeIdentifier"> + <PropertyGroup> + <UsingMicrosoftNETSdkBlazorWebAssembly>true</UsingMicrosoftNETSdkBlazorWebAssembly> + </PropertyGroup> + + <PropertyGroup> + <_BlazorWebAssemblyPropsFile Condition="'$(_BlazorWebAssemblyPropsFile)' == ''">$(MSBuildThisFileDirectory)..\targets\Microsoft.NET.Sdk.BlazorWebAssembly.Current.props</_BlazorWebAssemblyPropsFile> + </PropertyGroup> + + <Import Project="$(_BlazorWebAssemblyPropsFile)" /> +</Project> diff --git a/src/Components/WebAssembly/Sdk/src/Sdk/Sdk.targets b/src/Components/WebAssembly/Sdk/src/Sdk/Sdk.targets new file mode 100644 index 0000000000000000000000000000000000000000..616c56fb8a0b060e3972526176036f0cd0c35e10 --- /dev/null +++ b/src/Components/WebAssembly/Sdk/src/Sdk/Sdk.targets @@ -0,0 +1,20 @@ +<!-- +*********************************************************************************************** +Sdk.targets + +WARNING: DO NOT MODIFY this file unless you are knowledgeable about MSBuild and have + created a backup copy. Incorrect changes to this file will make it + impossible to load or build your projects from the command-line or the IDE. + +Copyright (c) .NET Foundation. All rights reserved. +*********************************************************************************************** +--> +<Project ToolsVersion="14.0"> + + <PropertyGroup> + <_BlazorWebAssemblyTargetsFile Condition="'$(_BlazorWebAssemblyTargetsFile)' == ''">$(MSBuildThisFileDirectory)..\targets\Microsoft.NET.Sdk.BlazorWebAssembly.Current.targets</_BlazorWebAssemblyTargetsFile> + </PropertyGroup> + + <Import Project="$(_BlazorWebAssemblyTargetsFile)" /> + +</Project> diff --git a/src/Razor/test/testassets/razorclasslibrary/wwwroot/wwwroot/exampleJsInterop.js b/src/Components/WebAssembly/Sdk/src/_._ similarity index 100% rename from src/Razor/test/testassets/razorclasslibrary/wwwroot/wwwroot/exampleJsInterop.js rename to src/Components/WebAssembly/Sdk/src/_._ diff --git a/src/Components/WebAssembly/Sdk/src/build/net5.0/Microsoft.NET.Sdk.BlazorWebAssembly.props b/src/Components/WebAssembly/Sdk/src/build/net5.0/Microsoft.NET.Sdk.BlazorWebAssembly.props new file mode 100644 index 0000000000000000000000000000000000000000..27e3fde3e00048ebf61c45aadda356bd0f35caf7 --- /dev/null +++ b/src/Components/WebAssembly/Sdk/src/build/net5.0/Microsoft.NET.Sdk.BlazorWebAssembly.props @@ -0,0 +1,16 @@ +<!-- +*********************************************************************************************** +Microsoft.NET.Sdk.BlazorWebAssembly.props + +WARNING: DO NOT MODIFY this file unless you are knowledgeable about MSBuild and have + created a backup copy. Incorrect changes to this file will make it + impossible to load or build your projects from the command-line or the IDE. + +Copyright (c) .NET Foundation. All rights reserved. +*********************************************************************************************** +--> +<Project ToolsVersion="14.0"> + <PropertyGroup> + <_BlazorWebAssemblyPropsFile>$(MSBuildThisFileDirectory)..\..\targets\Microsoft.NET.Sdk.BlazorWebAssembly.Current.props</_BlazorWebAssemblyPropsFile> + </PropertyGroup> +</Project> diff --git a/src/Components/WebAssembly/Sdk/src/build/net5.0/Microsoft.NET.Sdk.BlazorWebAssembly.targets b/src/Components/WebAssembly/Sdk/src/build/net5.0/Microsoft.NET.Sdk.BlazorWebAssembly.targets new file mode 100644 index 0000000000000000000000000000000000000000..8091b3d876f795eb899deef51912c0d540f1d25d --- /dev/null +++ b/src/Components/WebAssembly/Sdk/src/build/net5.0/Microsoft.NET.Sdk.BlazorWebAssembly.targets @@ -0,0 +1,16 @@ +<!-- +*********************************************************************************************** +Microsoft.NET.Sdk.BlazorWebAssembly.props + +WARNING: DO NOT MODIFY this file unless you are knowledgeable about MSBuild and have + created a backup copy. Incorrect changes to this file will make it + impossible to load or build your projects from the command-line or the IDE. + +Copyright (c) .NET Foundation. All rights reserved. +*********************************************************************************************** +--> +<Project ToolsVersion="14.0"> + <PropertyGroup> + <_BlazorWebAssemblyTargetsFile>$(MSBuildThisFileDirectory)..\..\targets\Microsoft.NET.Sdk.BlazorWebAssembly.Current.targets</_BlazorWebAssemblyTargetsFile> + </PropertyGroup> +</Project> diff --git a/src/Components/WebAssembly/Sdk/src/targets/BlazorWasm.web.config b/src/Components/WebAssembly/Sdk/src/targets/BlazorWasm.web.config new file mode 100644 index 0000000000000000000000000000000000000000..7f9995d792c3df47a6b92c4d598e44a2715be78c --- /dev/null +++ b/src/Components/WebAssembly/Sdk/src/targets/BlazorWasm.web.config @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="UTF-8"?> +<configuration> + <system.webServer> + <staticContent> + <remove fileExtension=".dat" /> + <remove fileExtension=".dll" /> + <remove fileExtension=".json" /> + <remove fileExtension=".wasm" /> + <remove fileExtension=".woff" /> + <remove fileExtension=".woff2" /> + <mimeMap fileExtension=".dll" mimeType="application/octet-stream" /> + <mimeMap fileExtension=".dat" mimeType="application/octet-stream" /> + <mimeMap fileExtension=".json" mimeType="application/json" /> + <mimeMap fileExtension=".wasm" mimeType="application/wasm" /> + <mimeMap fileExtension=".woff" mimeType="application/font-woff" /> + <mimeMap fileExtension=".woff2" mimeType="application/font-woff" /> + </staticContent> + <httpCompression> + <dynamicTypes> + <add mimeType="application/octet-stream" enabled="true" /> + <add mimeType="application/wasm" enabled="true" /> + </dynamicTypes> + </httpCompression> + <rewrite> + <rules> + <rule name="Serve subdir"> + <match url=".*" /> + <action type="Rewrite" url="wwwroot\{R:0}" /> + </rule> + <rule name="SPA fallback routing" stopProcessing="true"> + <match url=".*" /> + <conditions logicalGrouping="MatchAll"> + <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" /> + </conditions> + <action type="Rewrite" url="wwwroot\" /> + </rule> + </rules> + </rewrite> + </system.webServer> +</configuration> diff --git a/src/Components/WebAssembly/Sdk/src/targets/LinkerWorkaround.xml b/src/Components/WebAssembly/Sdk/src/targets/LinkerWorkaround.xml new file mode 100644 index 0000000000000000000000000000000000000000..c61bc7a3cca2d6c976e9c578a8aa4d0960333695 --- /dev/null +++ b/src/Components/WebAssembly/Sdk/src/targets/LinkerWorkaround.xml @@ -0,0 +1,15 @@ +<linker> + + <!-- This file specifies which parts of the BCL or Blazor packages must not be stripped + by the IL linker even if they are not referenced by user code. The file format is + described at https://github.com/mono/linker/blob/master/src/linker/README.md#syntax-of-xml-descriptor --> + + <assembly fullname="System"> + <!-- Without this, [Required(typeof(bool), "true", "true", ErrorMessage = "...")] fails --> + <type fullname="System.ComponentModel.BooleanConverter" /> + + <!-- TypeConverters are only used through reflection. These are two built-in TypeConverters that are useful. --> + <type fullname="System.ComponentModel.GuidConverter" /> + <type fullname="System.ComponentModel.TimeSpanConverter" /> + </assembly> +</linker> diff --git a/src/Components/WebAssembly/Sdk/src/targets/Microsoft.NET.Sdk.BlazorWebAssembly.Current.props b/src/Components/WebAssembly/Sdk/src/targets/Microsoft.NET.Sdk.BlazorWebAssembly.Current.props new file mode 100644 index 0000000000000000000000000000000000000000..03b94ad5666bf9b21695a4befdeeb092f60c964f --- /dev/null +++ b/src/Components/WebAssembly/Sdk/src/targets/Microsoft.NET.Sdk.BlazorWebAssembly.Current.props @@ -0,0 +1,36 @@ +<!-- +*********************************************************************************************** +Microsoft.NET.Sdk.BlazorWebAssembly.props + +WARNING: DO NOT MODIFY this file unless you are knowledgeable about MSBuild and have + created a backup copy. Incorrect changes to this file will make it + impossible to load or build your projects from the command-line or the IDE. + +Copyright (c) .NET Foundation. All rights reserved. +*********************************************************************************************** +--> +<Project ToolsVersion="14.0" TreatAsLocalProperty="RuntimeIdentifier"> + <PropertyGroup> + <!-- Blazor WASM projects are always browser-wasm --> + <RuntimeIdentifier>browser-wasm</RuntimeIdentifier> + + <!-- Avoid having the rid show up in output paths --> + <AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath> + + <OutputType>exe</OutputType> + + <IsPackable>false</IsPackable> + + <WarnOnPackingNonPackableProject>false</WarnOnPackingNonPackableProject> + </PropertyGroup> + + <PropertyGroup> + <!-- Determines if this Sdk is responsible for importing Microsoft.NET.Sdk.Razor. Temporary workaround until we can create a SDK. --> + <_RazorSdkImportsMicrosoftNetSdkRazor Condition="'$(UsingMicrosoftNETSdkRazor)' != 'true'">true</_RazorSdkImportsMicrosoftNetSdkRazor> + </PropertyGroup> + + <Import Sdk="Microsoft.NET.Sdk.Razor" Project="Sdk.props" Condition="'$(_RazorSdkImportsMicrosoftNetSdkRazor)' == 'true'" /> + <Import Sdk="Microsoft.NET.Sdk.Web.ProjectSystem" Project="Sdk.props" /> + <Import Sdk="Microsoft.NET.Sdk.Publish" Project="Sdk.props" /> + +</Project> diff --git a/src/Components/WebAssembly/Sdk/src/targets/Microsoft.NET.Sdk.BlazorWebAssembly.Current.targets b/src/Components/WebAssembly/Sdk/src/targets/Microsoft.NET.Sdk.BlazorWebAssembly.Current.targets new file mode 100644 index 0000000000000000000000000000000000000000..a11bc00625c2037367952498632c027696657965 --- /dev/null +++ b/src/Components/WebAssembly/Sdk/src/targets/Microsoft.NET.Sdk.BlazorWebAssembly.Current.targets @@ -0,0 +1,582 @@ +<!-- +*********************************************************************************************** +Microsoft.NET.Sdk.BlazorWebAssembly.targets + +WARNING: DO NOT MODIFY this file unless you are knowledgeable about MSBuild and have + created a backup copy. Incorrect changes to this file will make it + impossible to load or build your projects from the command-line or the IDE. + +Copyright (c) .NET Foundation. All rights reserved. +*********************************************************************************************** +--> +<Project ToolsVersion="14.0"> + + <PropertyGroup> + <EnableDefaultContentItems Condition=" '$(EnableDefaultContentItems)' == '' ">true</EnableDefaultContentItems> + </PropertyGroup> + + <Import Sdk="Microsoft.NET.Sdk.Razor" Project="Sdk.targets" Condition="'$(_RazorSdkImportsMicrosoftNetSdkRazor)' == 'true'" /> + <Import Sdk="Microsoft.NET.Sdk.Web.ProjectSystem" Project="Sdk.targets" /> + <Import Sdk="Microsoft.NET.Sdk.Publish" Project="Sdk.targets" /> + + <!-- + Targets supporting Razor MSBuild integration. Contain support for generating C# code using Razor + and including the generated code in the project lifecycle, including compiling, publishing and producing + nuget packages. + --> + + <!-- + This is a hook to import a set of targets before the Blazor targets. By default this is unused. + --> + <Import Project="$(CustomBeforeBlazorWebAssemblySdkTargets)" Condition="'$(CustomBeforeBlazorWebAssemblySdkTargets)' != '' and Exists('$(CustomBeforeBlazorWebAssemblySdkTargets)')"/> + + <PropertyGroup> + <!-- Paths to tools, tasks, and extensions are calculated relative to the BlazorWebAssemblySdkDirectoryRoot. This can be modified to test a local build. --> + <BlazorWebAssemblySdkDirectoryRoot Condition="'$(BlazorWebAssemblySdkDirectoryRoot)'==''">$(MSBuildThisFileDirectory)..\..\</BlazorWebAssemblySdkDirectoryRoot> + <_BlazorWebAssemblySdkTasksTFM Condition=" '$(MSBuildRuntimeType)' == 'Core'">net5.0</_BlazorWebAssemblySdkTasksTFM> + <_BlazorWebAssemblySdkTasksTFM Condition=" '$(MSBuildRuntimeType)' != 'Core'">net46</_BlazorWebAssemblySdkTasksTFM> + <_BlazorWebAssemblySdkTasksAssembly>$(BlazorWebAssemblySdkDirectoryRoot)tasks\$(_BlazorWebAssemblySdkTasksTFM)\Microsoft.NET.Sdk.BlazorWebAssembly.Tasks.dll</_BlazorWebAssemblySdkTasksAssembly> + <_BlazorWebAssemblySdkToolAssembly>$(BlazorWebAssemblySdkDirectoryRoot)tools\net5.0\Microsoft.NET.Sdk.BlazorWebAssembly.Tools.dll</_BlazorWebAssemblySdkToolAssembly> + </PropertyGroup> + + <UsingTask TaskName="Microsoft.NET.Sdk.BlazorWebAssembly.GenerateBlazorWebAssemblyBootJson" AssemblyFile="$(_BlazorWebAssemblySdkTasksAssembly)" /> + <UsingTask TaskName="Microsoft.NET.Sdk.BlazorWebAssembly.BlazorWriteSatelliteAssemblyFile" AssemblyFile="$(_BlazorWebAssemblySdkTasksAssembly)" /> + <UsingTask TaskName="Microsoft.NET.Sdk.BlazorWebAssembly.BlazorReadSatelliteAssemblyFile" AssemblyFile="$(_BlazorWebAssemblySdkTasksAssembly)" /> + <UsingTask TaskName="Microsoft.NET.Sdk.BlazorWebAssembly.BrotliCompress" AssemblyFile="$(_BlazorWebAssemblySdkTasksAssembly)" /> + <UsingTask TaskName="Microsoft.NET.Sdk.BlazorWebAssembly.GzipCompress" AssemblyFile="$(_BlazorWebAssemblySdkTasksAssembly)" /> + <UsingTask TaskName="Microsoft.NET.Sdk.BlazorWebAssembly.CreateBlazorTrimmerRootDescriptorFile" AssemblyFile="$(_BlazorWebAssemblySdkTasksAssembly)" /> + + <PropertyGroup> + <SelfContained>true</SelfContained> + <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies> + + <!-- Trimmer defaults --> + <PublishTrimmed Condition="'$(PublishTrimmed)' == ''">true</PublishTrimmed> + <TrimMode Condition="'$(TrimMode)' == ''">link</TrimMode> + + <StaticWebAssetBasePath Condition="'$(StaticWebAssetBasePath)' == ''">/</StaticWebAssetBasePath> + <BlazorCacheBootResources Condition="'$(BlazorCacheBootResources)' == ''">true</BlazorCacheBootResources> + + <!-- Turn off parts of the build that do not apply to WASM projects --> + <GenerateDependencyFile>false</GenerateDependencyFile> + <GenerateRuntimeConfigurationFiles>false</GenerateRuntimeConfigurationFiles> + <PreserveCompilationContext>false</PreserveCompilationContext> + <PreserveCompilationReferences>false</PreserveCompilationReferences> + <IsWebConfigTransformDisabled>true</IsWebConfigTransformDisabled> + + <!-- Internal properties --> + <_BlazorOutputPath>wwwroot\_framework\</_BlazorOutputPath> + + </PropertyGroup> + + <Import Project="Microsoft.NET.Sdk.BlazorWebAssembly.ServiceWorkerAssetsManifest.targets" /> + + <Target Name="_ScrambleDotnetJsFileName" AfterTargets="ResolveRuntimePackAssets"> + <!-- + We want the dotnet.js file output to have a version to better work with caching. We'll append the runtime version to the file name as soon as file has been discovered. + --> + <PropertyGroup> + <_DotNetJsVersion>$(BundledNETCoreAppPackageVersion)</_DotNetJsVersion> + <_DotNetJsVersion Condition="'$(RuntimeFrameworkVersion)' != ''">$(RuntimeFrameworkVersion)</_DotNetJsVersion> + <_BlazorDotnetJsFileName>dotnet.$(_DotNetJsVersion).js</_BlazorDotnetJsFileName> + <_BlazorDotNetJsFilePath>$(IntermediateOutputPath)$(_BlazorDotnetJsFileName)</_BlazorDotNetJsFilePath> + </PropertyGroup> + + <ItemGroup> + <_DotNetJsItem Include="@(ReferenceCopyLocalPaths)" Condition="'%(ReferenceCopyLocalPaths.DestinationSubPath)' == 'dotnet.js' AND '%(ReferenceCopyLocalPaths.AssetType)' == 'native'" /> + </ItemGroup> + + <Copy + SourceFiles="@(_DotNetJsItem)" + DestinationFiles="$(_BlazorDotNetJsFilePath)" + SkipUnchangedFiles="true" + OverwriteReadOnlyFiles="$(OverwriteReadOnlyFiles)" /> + + <ItemGroup Condition="'@(_DotNetJsItem->Count())' != '0'"> + <ReferenceCopyLocalPaths + Include="$(_BlazorDotNetJsFilePath)" + AssetType="native" + CopyLocal="true" + DestinationSubPath="$(_BlazorDotnetJsFileName)" /> + + <ReferenceCopyLocalPaths Remove="@(_DotNetJsItem)" /> + </ItemGroup> + </Target> + + <Target Name="_ResolveBlazorWasmOutputs" DependsOnTargets="ResolveReferences;PrepareResourceNames;ComputeIntermediateSatelliteAssemblies"> + <!-- + Calculates the outputs and the paths for Blazor WASM. This target is invoked frequently and should perform minimal work. + --> + + <PropertyGroup> + <_BlazorSatelliteAssemblyCacheFile>$(IntermediateOutputPath)blazor.satelliteasm.props</_BlazorSatelliteAssemblyCacheFile> + <!-- Workaround for https://github.com/dotnet/sdk/issues/12114--> + <PublishDir Condition="'$(AppendRuntimeIdentifierToOutputPath)' != 'true' AND '$(PublishDir)' == '$(OutputPath)$(RuntimeIdentifier)\$(PublishDirName)\'">$(OutputPath)$(PublishDirName)\</PublishDir> + </PropertyGroup> + + <ItemGroup> + <_BlazorJSFile Include="$(BlazorWebAssemblyJSPath)" /> + <_BlazorJSFile Include="$(BlazorWebAssemblyJSMapPath)" Condition="Exists('$(BlazorWebAssemblyJSMapPath)')" /> + + <_BlazorConfigFile Include="wwwroot\appsettings*.json" /> + + <!-- Clear out temporary build artifacts that the runtime packages --> + <ReferenceCopyLocalPaths Remove="@(ReferenceCopyLocalPaths)" Condition="'%(ReferenceCopyLocalPaths.Extension)' == '.a'" /> + + <!-- + ReferenceCopyLocalPaths includes satellite assemblies from referenced projects but are inexpicably missing + any metadata that might allow them to be differentiated. We'll explicitly add those + to _BlazorOutputWithTargetPath so that satellite assemblies from packages, the current project and referenced project + are all treated the same. + --> + + <_BlazorCopyLocalPath + Include="@(ReferenceCopyLocalPaths)" + Exclude="@(ReferenceSatellitePaths)"/> + + <_BlazorCopyLocalPath Include="@(IntermediateSatelliteAssembliesWithTargetPath)"> + <DestinationSubDirectory>%(IntermediateSatelliteAssembliesWithTargetPath.Culture)\</DestinationSubDirectory> + </_BlazorCopyLocalPath> + + <_BlazorOutputWithTargetPath Include=" + @(_BlazorCopyLocalPath); + @(IntermediateAssembly); + @(_DebugSymbolsIntermediatePath); + @(_BlazorJSFile)" /> + + <_BlazorOutputWithTargetPath Include="@(ReferenceSatellitePaths)"> + <Culture>$([System.String]::Copy('%(ReferenceSatellitePaths.DestinationSubDirectory)').Trim('\').Trim('/'))</Culture> + </_BlazorOutputWithTargetPath> + </ItemGroup> + + <!-- + BuildingProject=false is typically set for referenced projects when building inside VisualStudio. + + When building with BuildingProject=false, satellite assemblies do not get resolved (the ones for the current project and the one for + referenced project). Satellite assemblies from packages get resolved. + To workaround this, we'll cache metadata during a regular build, and rehydrate from it when BuildingProject=false. + --> + <BlazorReadSatelliteAssemblyFile + ReadFile="$(_BlazorSatelliteAssemblyCacheFile)" + Condition="'$(BuildingProject)' != 'true' AND EXISTS('$(_BlazorSatelliteAssemblyCacheFile)')"> + <Output TaskParameter="SatelliteAssembly" ItemName="_BlazorReadSatelliteAssembly" /> + </BlazorReadSatelliteAssemblyFile> + + <ItemGroup> + <!-- We've imported a previously Cacheed file. Let's turn in to a _BlazorOutputWithTargetPath --> + <_BlazorOutputWithTargetPath + Include="@(_BlazorReadSatelliteAssembly)" + Exclude="@(_BlazorOutputWithTargetPath)" + Condition="'@(_BlazorReadSatelliteAssembly->Count())' != '0'" /> + + <!-- Calculate the target path --> + <_BlazorOutputWithTargetPath + TargetPath="$(_BlazorOutputPath)%(_BlazorOutputWithTargetPath.DestinationSubDirectory)%(FileName)%(Extension)" + Condition="'%(__BlazorOutputWithTargetPath.TargetPath)' == ''" /> + </ItemGroup> + </Target> + + <Target Name="_ProcessBlazorWasmOutputs" DependsOnTargets="_ResolveBlazorWasmOutputs"> + <PropertyGroup> + <_BlazorBuildGZipCompressDirectory>$(IntermediateOutputPath)build-gz\</_BlazorBuildGZipCompressDirectory> + </PropertyGroup> + + <!-- + Compress referenced binaries using GZip during build. This skips files such as the project's assemblies + that change from build to build. Runtime assets contribute to the bulk of the download size. Compressing it + has the most benefit while avoiding any ongoing costs to the dev inner loop. + --> + <ItemGroup> + <_GzipFileToCompressForBuild + Include="@(ReferenceCopyLocalPaths)" + RelativePath="$(_BlazorOutputPath)%(ReferenceCopyLocalPaths.DestinationSubDirectory)%(FileName)%(Extension)" + Condition="'%(Extension)' == '.dll' or '%(ReferenceCopyLocalPaths.AssetType)' == 'native'" /> + </ItemGroup> + + <GZipCompress + FilesToCompress="@(_GzipFileToCompressForBuild)" + OutputDirectory="$(_BlazorBuildGZipCompressDirectory)"> + + <Output TaskParameter="CompressedFiles" ItemName="_BlazorBuildGZipCompressedFile" /> + <Output TaskParameter="CompressedFiles" ItemName="FileWrites" /> + </GZipCompress> + + <ItemGroup> + <_BlazorWriteSatelliteAssembly Include="@(_BlazorOutputWithTargetPath->HasMetadata('Culture'))" /> + + <!-- Retarget ReferenceCopyLocalPaths to copy to the wwwroot directory --> + <ReferenceCopyLocalPaths DestinationSubDirectory="$(_BlazorOutputPath)%(ReferenceCopyLocalPaths.DestinationSubDirectory)" /> + </ItemGroup> + + <!-- A missing blazor.webassembly.js is our packaging error. Produce an error so it's discovered early. --> + <Error + Text="Unable to find BlazorWebAssembly JS files. This usually indicates a packaging error." + Code="RAZORSDK1007" + Condition="'@(_BlazorJSFile->Count())' == '0'" /> + + <!-- + When building with BuildingProject=false, satellite assemblies do not get resolved (the ones for the current project and the one for + referenced project). BuildingProject=false is typically set for referenced projects when building inside VisualStudio. + To workaround this, we'll cache metadata during a regular build, and rehydrate from it when BuildingProject=false. + --> + + <BlazorWriteSatelliteAssemblyFile + SatelliteAssembly="@(_BlazorWriteSatelliteAssembly)" + WriteFile="$(_BlazorSatelliteAssemblyCacheFile)" + Condition="'$(BuildingProject)' == 'true' AND '@(_BlazorWriteSatelliteAssembly->Count())' != '0'" /> + + <Delete + Files="$(_BlazorSatelliteAssemblyCacheFile)" + Condition="'$(BuildingProject)' == 'true' AND '@(_BlazorWriteSatelliteAssembly->Count())' == '0' and EXISTS('$(_BlazorSatelliteAssemblyCacheFile)')" /> + + <ItemGroup> + <FileWrites Include="$(_BlazorSatelliteAssemblyCacheFile)" Condition="Exists('$(_BlazorSatelliteAssemblyCacheFile)')" /> + </ItemGroup> + + <GetFileHash Files="@(_BlazorOutputWithTargetPath)" Algorithm="SHA256" HashEncoding="base64"> + <Output TaskParameter="Items" ItemName="_BlazorOutputWithHash" /> + </GetFileHash> + </Target> + + <PropertyGroup> + <PrepareForRunDependsOn> + _BlazorWasmPrepareForRun; + $(PrepareForRunDependsOn) + </PrepareForRunDependsOn> + + <GetCurrentProjectStaticWebAssetsDependsOn> + $(GetCurrentProjectStaticWebAssetsDependsOn); + _BlazorWasmPrepareForRun; + </GetCurrentProjectStaticWebAssetsDependsOn> + </PropertyGroup> + + <Target Name="_BlazorWasmPrepareForRun" DependsOnTargets="_ProcessBlazorWasmOutputs" BeforeTargets="_RazorPrepareForRun" AfterTargets="GetCurrentProjectStaticWebAssets"> + <PropertyGroup> + <_BlazorBuildBootJsonPath>$(IntermediateOutputPath)blazor.boot.json</_BlazorBuildBootJsonPath> + </PropertyGroup> + + <GenerateBlazorWebAssemblyBootJson + AssemblyPath="@(IntermediateAssembly)" + Resources="@(_BlazorOutputWithHash)" + DebugBuild="true" + LinkerEnabled="false" + CacheBootResources="$(BlazorCacheBootResources)" + OutputPath="$(_BlazorBuildBootJsonPath)" + ConfigurationFiles="@(_BlazorConfigFile)" + LazyLoadedAssemblies="@(BlazorWebAssemblyLazyLoad)" /> + + <ItemGroup> + <FileWrites Include="$(OutDir)$(_BlazorOutputPath)blazor.boot.json" /> + </ItemGroup> + + <ItemGroup> + <_BlazorWebAssemblyStaticWebAsset Include="$(_BlazorBuildBootJsonPath)"> + <SourceId>$(PackageId)</SourceId> + <SourceType></SourceType> + <ContentRoot>$([MSBuild]::NormalizeDirectory('$(TargetDir)wwwroot\'))</ContentRoot> + <BasePath>$(StaticWebAssetBasePath)</BasePath> + <RelativePath>_framework/blazor.boot.json</RelativePath> + <CopyToPublishDirectory>Never</CopyToPublishDirectory> + </_BlazorWebAssemblyStaticWebAsset> + + <_BlazorWebAssemblyStaticWebAsset Include="@(_BlazorOutputWithHash)"> + <SourceId>$(PackageId)</SourceId> + <SourceType></SourceType> + <ContentRoot>$([MSBuild]::NormalizeDirectory('$(TargetDir)wwwroot\'))</ContentRoot> + <BasePath>$(StaticWebAssetBasePath)</BasePath> + <RelativePath>$([System.String]::Copy('%(_BlazorOutputWithHash.TargetPath)').Replace('\','/').Substring(8))</RelativePath> + <CopyToPublishDirectory>Never</CopyToPublishDirectory> + </_BlazorWebAssemblyStaticWebAsset> + + <_BlazorWebAssemblyStaticWebAsset Include="@(_BlazorBuildGZipCompressedFile)"> + <SourceId>$(PackageId)</SourceId> + <SourceType></SourceType> + <ContentRoot>$([MSBuild]::NormalizeDirectory('$(TargetDir)wwwroot\'))</ContentRoot> + <BasePath>$(StaticWebAssetBasePath)</BasePath> + <RelativePath>$([System.String]::Copy('%(_BlazorBuildGZipCompressedFile.RelativePath)').Replace('\','/').Substring(8))</RelativePath> + <CopyToPublishDirectory>Never</CopyToPublishDirectory> + </_BlazorWebAssemblyStaticWebAsset> + + <StaticWebAsset Include="@(_BlazorWebAssemblyStaticWebAsset)" /> + <_ExternalStaticWebAsset Include="@(_BlazorWebAssemblyStaticWebAsset)" SourceType="Generated" /> + </ItemGroup> + </Target> + + <!-- Mimics the behavior of CopyFilesToOutputDirectory. We simply copy relevant build outputs to the wwwroot directory --> + <Target Name="_BlazorCopyFilesToOutputDirectory" AfterTargets="CopyFilesToOutputDirectory"> + <Copy + SourceFiles="@(IntermediateAssembly)" + DestinationFolder="$(OutDir)$(_BlazorOutputPath)" + SkipUnchangedFiles="$(SkipCopyUnchangedFiles)" + OverwriteReadOnlyFiles="$(OverwriteReadOnlyFiles)" + Retries="$(CopyRetryCount)" + RetryDelayMilliseconds="$(CopyRetryDelayMilliseconds)" + UseHardlinksIfPossible="$(CreateHardLinksForCopyFilesToOutputDirectoryIfPossible)" + UseSymboliclinksIfPossible="$(CreateSymbolicLinksForCopyFilesToOutputDirectoryIfPossible)" + ErrorIfLinkFails="$(ErrorIfLinkFailsForCopyFilesToOutputDirectory)" + Condition="'$(CopyBuildOutputToOutputDirectory)' == 'true' and '$(SkipCopyBuildProduct)' != 'true'"> + + <Output TaskParameter="DestinationFiles" ItemName="FileWrites"/> + </Copy> + + <Message Importance="High" Text="$(MSBuildProjectName) (Blazor output) -> $(TargetDir)wwwroot" Condition="'$(CopyBuildOutputToOutputDirectory)' == 'true' and '$(SkipCopyBuildProduct)'!='true'" /> + + <Copy + SourceFiles="@(_DebugSymbolsIntermediatePath)" + DestinationFolder="$(OutDir)$(_BlazorOutputPath)" + SkipUnchangedFiles="$(SkipCopyUnchangedFiles)" + OverwriteReadOnlyFiles="$(OverwriteReadOnlyFiles)" + Retries="$(CopyRetryCount)" + RetryDelayMilliseconds="$(CopyRetryDelayMilliseconds)" + UseHardlinksIfPossible="$(CreateHardLinksForCopyFilesToOutputDirectoryIfPossible)" + UseSymboliclinksIfPossible="$(CreateSymbolicLinksForCopyFilesToOutputDirectoryIfPossible)" + ErrorIfLinkFails="$(ErrorIfLinkFailsForCopyFilesToOutputDirectory)" + Condition="'$(_DebugSymbolsProduced)'=='true' and '$(SkipCopyingSymbolsToOutputDirectory)' != 'true' and '$(CopyOutputSymbolsToOutputDirectory)'=='true'"> + + <Output TaskParameter="DestinationFiles" ItemName="FileWrites"/> + </Copy> + + <Copy + SourceFiles="@(IntermediateSatelliteAssembliesWithTargetPath)" + DestinationFiles="@(IntermediateSatelliteAssembliesWithTargetPath->'$(OutDir)$(_BlazorOutputPath)%(Culture)\$(TargetName).resources.dll')" + SkipUnchangedFiles="$(SkipCopyUnchangedFiles)" + OverwriteReadOnlyFiles="$(OverwriteReadOnlyFiles)" + Retries="$(CopyRetryCount)" + RetryDelayMilliseconds="$(CopyRetryDelayMilliseconds)" + UseHardlinksIfPossible="$(CreateHardLinksForCopyFilesToOutputDirectoryIfPossible)" + UseSymboliclinksIfPossible="$(CreateSymbolicLinksForCopyFilesToOutputDirectoryIfPossible)" + ErrorIfLinkFails="$(ErrorIfLinkFailsForCopyFilesToOutputDirectory)" + Condition="'@(IntermediateSatelliteAssembliesWithTargetPath)' != ''" > + + <Output TaskParameter="DestinationFiles" ItemName="FileWrites"/> + </Copy> + + <Copy + SourceFiles="@(_BlazorJSFile);$(_BlazorBuildBootJsonPath)" + DestinationFolder="$(OutDir)$(_BlazorOutputPath)" + SkipUnchangedFiles="$(SkipCopyUnchangedFiles)" + OverwriteReadOnlyFiles="$(OverwriteReadOnlyFiles)" + Retries="$(CopyRetryCount)" + RetryDelayMilliseconds="$(CopyRetryDelayMilliseconds)" + UseHardlinksIfPossible="$(CreateHardLinksForCopyFilesToOutputDirectoryIfPossible)" + UseSymboliclinksIfPossible="$(CreateSymbolicLinksForCopyFilesToOutputDirectoryIfPossible)" + ErrorIfLinkFails="$(ErrorIfLinkFailsForCopyFilesToOutputDirectory)"> + + <Output TaskParameter="DestinationFiles" ItemName="FileWrites"/> + </Copy> + + <Copy + SourceFiles="@(_BlazorBuildGZipCompressedFile)" + DestinationFiles="@(_BlazorBuildGZipCompressedFile->'$(OutDir)%(RelativePath)')" + SkipUnchangedFiles="$(SkipCopyUnchangedFiles)" + OverwriteReadOnlyFiles="$(OverwriteReadOnlyFiles)" + Retries="$(CopyRetryCount)" + RetryDelayMilliseconds="$(CopyRetryDelayMilliseconds)" + UseHardlinksIfPossible="$(CreateHardLinksForCopyFilesToOutputDirectoryIfPossible)" + UseSymboliclinksIfPossible="$(CreateSymbolicLinksForCopyFilesToOutputDirectoryIfPossible)" + ErrorIfLinkFails="$(ErrorIfLinkFailsForCopyFilesToOutputDirectory)"> + + <Output TaskParameter="DestinationFiles" ItemName="FileWrites"/> + </Copy> + </Target> + + <Target Name="_BlazorWasmPrepareForLink" BeforeTargets="PrepareForILLink"> + <PropertyGroup> + <_BlazorTypeGranularTrimmerDescriptorFile>$(IntermediateOutputPath)typegranularity.trimmerdescriptor.xml</_BlazorTypeGranularTrimmerDescriptorFile> + </PropertyGroup> + + <ItemGroup> + <_BlazorTypeGranularAssembly + Include="@(ManagedAssemblyToLink)" + Condition="'%(Extension)' == '.dll' AND ($([System.String]::Copy('%(Filename)').StartsWith('Microsoft.AspNetCore.')) or $([System.String]::Copy('%(Filename)').StartsWith('Microsoft.Extensions.')))"> + <Required>false</Required> + <Preserve>all</Preserve> + </_BlazorTypeGranularAssembly> + + <ManagedAssemblyToLink + IsTrimmable="true" + Condition="'%(Extension)' == '.dll' AND ($([System.String]::Copy('%(Filename)').StartsWith('Microsoft.AspNetCore.')) or $([System.String]::Copy('%(Filename)').StartsWith('Microsoft.Extensions.')))" /> + </ItemGroup> + + <CreateBlazorTrimmerRootDescriptorFile + Assemblies="@(_BlazorTypeGranularAssembly)" + TrimmerFile="$(_BlazorTypeGranularTrimmerDescriptorFile)" /> + + <ItemGroup> + <TrimmerRootDescriptor Include="$(_BlazorTypeGranularTrimmerDescriptorFile)" /> + <TrimmerRootDescriptor Include="$(MSBuildThisFileDirectory)LinkerWorkaround.xml" /> + + <FileWrites Include="$(_BlazorTypeGranularTrimmerDescriptorFile)" /> + </ItemGroup> + </Target> + + <Target Name="_ProcessPublishFilesForBlazor" DependsOnTargets="_ResolveBlazorWasmOutputs" AfterTargets="ILLink"> + + <!-- + ResolvedFileToPublish.Culture is missing for satellite assemblies from project references. + Since we need the culture to correctly generate blazor.boot.json, we cross-reference the culture we calculate as part of _ResolveBlazorWasmOutputs + --> + <JoinItems Left="@(ResolvedFileToPublish)" + Right="@(_BlazorOutputWithTargetPath->HasMetadata('Culture'))" + LeftMetadata="*" + RightMetadata="Culture" + ItemSpecToUse="Left"> + <Output TaskParameter="JoinResult" ItemName="_ResolvedSatelliteToPublish" /> + </JoinItems> + + <ItemGroup> + <ResolvedFileToPublish Remove="@(_ResolvedSatelliteToPublish)" /> + <ResolvedFileToPublish Include="@(_ResolvedSatelliteToPublish)" /> + + <ResolvedFileToPublish Remove="@(ResolvedFileToPublish)" Condition="'%(Extension)' == '.a'" /> + + <!-- Remove dotnet.js from publish output --> + <ResolvedFileToPublish Remove="@(ResolvedFileToPublish)" Condition="'%(ResolvedFileToPublish.RelativePath)' == 'dotnet.js'" /> + + <!-- Retarget so that items are published to the wwwroot directory --> + <ResolvedFileToPublish + RelativePath="$(_BlazorOutputPath)%(ResolvedFileToPublish.RelativePath)" + Condition="'%(ResolvedFileToPublish.RelativePath)' != 'web.config' AND !$([System.String]::Copy('%(ResolvedFileToPublish.RelativePath)').Replace('\','/').StartsWith('wwwroot/'))" /> + + <!-- Remove pdbs from the publish output --> + <ResolvedFileToPublish Remove="@(ResolvedFileToPublish)" Condition="'$(BlazorWebAssemblyEnableDebugging)' != 'true' AND '%(Extension)' == '.pdb'" /> + </ItemGroup> + + <ItemGroup Condition="'@(ResolvedFileToPublish->AnyHaveMetadataValue('RelativePath', 'web.config'))' != 'true'"> + <ResolvedFileToPublish + Include="$(MSBuildThisFileDirectory)BlazorWasm.web.config" + ExcludeFromSingleFile="true" + CopyToPublishDirectory="PreserveNewest" + RelativePath="web.config" /> + </ItemGroup> + + <!-- Generate the publish boot json --> + <ItemGroup> + <_BlazorPublishBootResource + Include="@(ResolvedFileToPublish)" + Condition="$([System.String]::Copy('%(RelativePath)').Replace('\','/').StartsWith('wwwroot/_framework')) AND '%(Extension)' != '.a'" /> + </ItemGroup> + + <GetFileHash Files="@(_BlazorPublishBootResource)" Algorithm="SHA256" HashEncoding="base64"> + <Output TaskParameter="Items" ItemName="_BlazorPublishBootResourceWithHash" /> + </GetFileHash> + + <GenerateBlazorWebAssemblyBootJson + AssemblyPath="@(IntermediateAssembly)" + Resources="@(_BlazorPublishBootResourceWithHash)" + DebugBuild="false" + LinkerEnabled="$(PublishTrimmed)" + CacheBootResources="$(BlazorCacheBootResources)" + OutputPath="$(IntermediateOutputPath)blazor.publish.boot.json" + ConfigurationFiles="@(_BlazorConfigFile)" + LazyLoadedAssemblies="@(BlazorWebAssemblyLazyLoad)" /> + + <ItemGroup> + <ResolvedFileToPublish + Include="$(IntermediateOutputPath)blazor.publish.boot.json" + RelativePath="$(_BlazorOutputPath)blazor.boot.json" /> + + <ResolvedFileToPublish + Include="@(_BlazorJSFile)" + RelativePath="$(_BlazorOutputPath)%(FileName)%(Extension)" /> + </ItemGroup> + </Target> + + <Target Name="_BlazorCompressPublishFiles" AfterTargets="_ProcessPublishFilesForBlazor" Condition="'$(BlazorEnableCompression)' != 'false'"> + <PropertyGroup> + <_CompressedFileOutputPath>$(IntermediateOutputPath)compress\</_CompressedFileOutputPath> + <_BlazorWebAssemblyBrotliIncremental>true</_BlazorWebAssemblyBrotliIncremental> + </PropertyGroup> + + <ItemGroup> + <_FileToCompress + Include="@(ResolvedFileToPublish)" + Condition="$([System.String]::Copy('%(ResolvedFileToPublish.RelativePath)').Replace('\','/').StartsWith('wwwroot/'))" /> + </ItemGroup> + + <Message Text="Compressing Blazor WebAssembly publish artifacts. This may take a while..." Importance="High" /> + + <MakeDir Directories="$(_CompressedFileOutputPath)" Condition="!Exists('$(_CompressedFileOutputPath)')" /> + + <PropertyGroup Condition="'$(DOTNET_HOST_PATH)' == ''"> + <_DotNetHostDirectory>$(NetCoreRoot)</_DotNetHostDirectory> + <_DotNetHostFileName>dotnet</_DotNetHostFileName> + <_DotNetHostFileName Condition="'$(OS)' == 'Windows_NT'">dotnet.exe</_DotNetHostFileName> + </PropertyGroup> + + <BrotliCompress + OutputDirectory="$(_CompressedFileOutputPath)" + FilesToCompress="@(_FileToCompress)" + CompressionLevel="$(_BlazorBrotliCompressionLevel)" + SkipIfOutputIsNewer="$(_BlazorWebAssemblyBrotliIncremental)" + ToolAssembly="$(_BlazorWebAssemblySdkToolAssembly)" + ToolExe="$(_DotNetHostFileName)" + ToolPath="$(_DotNetHostDirectory)"> + + <Output TaskParameter="CompressedFiles" ItemName="_BrotliCompressedFile" /> + <Output TaskParameter="CompressedFiles" ItemName="FileWrites" /> + </BrotliCompress> + + <GZipCompress + OutputDirectory="$(_CompressedFileOutputPath)" + FilesToCompress="@(_FileToCompress)"> + + <Output TaskParameter="CompressedFiles" ItemName="_BlazorPublishGZipCompressedFile" /> + <Output TaskParameter="CompressedFiles" ItemName="FileWrites" /> + </GZipCompress> + + <ItemGroup> + <ResolvedFileToPublish Include="@(_BrotliCompressedFile)" /> + <ResolvedFileToPublish Include="@(_BlazorPublishGZipCompressedFile)" /> + </ItemGroup> + </Target> + + <Target Name="_SetupPublishSemaphore" BeforeTargets="PrepareForPublish"> + <PropertyGroup> + <!-- + Add marker that indicates Blazor WASM is doing a publish. This is used to identify when GetCopyToPublishDirectoryItems + is invoked as a result of a P2P reference. + --> + <_PublishingBlazorWasmProject>true</_PublishingBlazorWasmProject> + </PropertyGroup> + </Target> + + <Target Name="_GetBlazorWasmFilesForPublishInner" + DependsOnTargets="_ResolveBlazorWasmOutputs;ComputeFilesToPublish" + Returns="@(ResolvedFileToPublish)" /> + + <Target Name="_GetBlazorWasmFilesForPublish" BeforeTargets="GetCopyToPublishDirectoryItems"> + <MSBuild + Projects="$(MSBuildProjectFullPath)" + Targets="_GetBlazorWasmFilesForPublishInner" + Properties="BuildProjectReferences=false;ResolveAssemblyReferencesFindRelatedSatellites=true;_PublishingBlazorWasmProject=true" + RemoveProperties="NoBuild;RuntimeIdentifier" + BuildInParallel="$(BuildInParallel)" + Condition="'$(_PublishingBlazorWasmProject)' != 'true'"> + + <Output TaskParameter="TargetOutputs" ItemName="_ResolvedFileToPublish" /> + </MSBuild> + + <ItemGroup> + <AllPublishItemsFullPathWithTargetPath Include="@(_ResolvedFileToPublish->'%(FullPath)')"> + <TargetPath>%(RelativePath)</TargetPath> + <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory> + </AllPublishItemsFullPathWithTargetPath> + </ItemGroup> + </Target> + + <Target Name="_BlazorApplyLinkPreferencesToContent" BeforeTargets="AssignTargetPaths;ResolveCurrentProjectStaticWebAssetsInputs;ResolveStaticWebAssetsInputs" Returns="@(Content)"> + <ItemGroup> + <Content + Condition="'%(Content.Link)' != '' AND '%(Content.CopyToPublishDirectory)' == '' AND $([System.String]::Copy('%(Content.Link)').Replace('\','/').StartsWith('wwwroot/'))" + CopyToPublishDirectory="PreserveNewest" /> + + </ItemGroup> + </Target> + + <!-- + This is a hook to import a set of targets after the Blazor WebAssembly targets. By default this is unused. + --> + <Import Project="$(CustomAfterBlazorWebAssemblySdkTargets)" Condition="'$(CustomAfterBlazorWebAssemblySdkTargets)' != '' and Exists('$(CustomAfterBlazorWebAssemblySdkTargets)')"/> + +</Project> diff --git a/src/Components/WebAssembly/Sdk/src/targets/Microsoft.NET.Sdk.BlazorWebAssembly.ServiceWorkerAssetsManifest.targets b/src/Components/WebAssembly/Sdk/src/targets/Microsoft.NET.Sdk.BlazorWebAssembly.ServiceWorkerAssetsManifest.targets new file mode 100644 index 0000000000000000000000000000000000000000..5202eb338d40c91814e5b24c2aeeebebd9d0a45b --- /dev/null +++ b/src/Components/WebAssembly/Sdk/src/targets/Microsoft.NET.Sdk.BlazorWebAssembly.ServiceWorkerAssetsManifest.targets @@ -0,0 +1,169 @@ +<!-- +*********************************************************************************************** +Microsoft.NET.Sdk.BlazorWebAssembly.ServiceWorkerAssetsManifest.targets + +WARNING: DO NOT MODIFY this file unless you are knowledgeable about MSBuild and have + created a backup copy. Incorrect changes to this file will make it + impossible to load or build your projects from the command-line or the IDE. + +Copyright (c) .NET Foundation. All rights reserved. +*********************************************************************************************** +--> + +<Project> + <UsingTask TaskName="Microsoft.NET.Sdk.BlazorWebAssembly.GenerateServiceWorkerAssetsManifest" AssemblyFile="$(_BlazorWebAssemblySdkTasksAssembly)" /> + + <Target Name="_ComputeServiceWorkerAssets" BeforeTargets="ResolveStaticWebAssetsInputs"> + + <PropertyGroup> + <_ServiceWorkerAssetsManifestIntermediateOutputPath Condition="'$([System.IO.Path]::IsPathRooted($(BaseIntermediateOutputPath)))' == 'true'">obj\$(Configuration)\$(TargetFramework)\$(ServiceWorkerAssetsManifest)</_ServiceWorkerAssetsManifestIntermediateOutputPath> + <_ServiceWorkerAssetsManifestIntermediateOutputPath Condition="'$([System.IO.Path]::IsPathRooted($(BaseIntermediateOutputPath)))' != 'true'">$(IntermediateOutputPath)$(ServiceWorkerAssetsManifest)</_ServiceWorkerAssetsManifestIntermediateOutputPath> + <_ServiceWorkerAssetsManifestFullPath>$([System.IO.Path]::GetFullPath('$(MSBuildProjectDirectory)/$(_ServiceWorkerAssetsManifestIntermediateOutputPath)'))</_ServiceWorkerAssetsManifestFullPath> + </PropertyGroup> + + <ItemGroup> + <_ManifestStaticWebAsset Include="$(_ServiceWorkerAssetsManifestFullPath)"> + <SourceType></SourceType> + <SourceId>$(PackageId)</SourceId> + <ContentRoot>$([MSBuild]::NormalizeDirectory('$(TargetDir)wwwroot\'))</ContentRoot> + <BasePath>$(StaticWebAssetBasePath)</BasePath> + <RelativePath>$(ServiceWorkerAssetsManifest)</RelativePath> + <CopyToPublishDirectory>Never</CopyToPublishDirectory> + </_ManifestStaticWebAsset> + + <!-- Figure out where we're getting the content for each @(ServiceWorker) entry, depending on whether there's a PublishedContent value --> + <_ServiceWorkerIntermediateFile Include="@(ServiceWorker->'$(IntermediateOutputPath)serviceworkers\%(Identity)')"> + <ContentSourcePath Condition="'%(_ServiceWorker.PublishedContent)' != ''">%(ServiceWorker.PublishedContent)</ContentSourcePath> + <ContentSourcePath Condition="'%(_ServiceWorker.PublishedContent)' == ''">%(ServiceWorker.Identity)</ContentSourcePath> + <OriginalPath>%(ServiceWorker.Identity)</OriginalPath> + <TargetOutputPath>%(ServiceWorker.Identity)</TargetOutputPath> + <TargetOutputPath Condition="$([System.String]::Copy('%(ServiceWorker.Identity)').Replace('\','/').StartsWith('wwwroot/'))">$([System.String]::Copy('%(ServiceWorker.Identity)').Substring(8))</TargetOutputPath> + </_ServiceWorkerIntermediateFile> + + <_ServiceWorkerStaticWebAsset Include="%(_ServiceWorkerIntermediateFile.FullPath)"> + <SourceType></SourceType> + <SourceId>$(PackageId)</SourceId> + <ContentRoot>$([MSBuild]::NormalizeDirectory('$(TargetDir)wwwroot\'))</ContentRoot> + <BasePath>$(StaticWebAssetBasePath)</BasePath> + <RelativePath>%(TargetOutputPath)</RelativePath> + <CopyToPublishDirectory>Never</CopyToPublishDirectory> + </_ServiceWorkerStaticWebAsset> + + <StaticWebAsset Include=" + @(_ManifestStaticWebAsset); + @(_ServiceWorkerStaticWebAsset)" /> + </ItemGroup> + + </Target> + + <Target Name="_WriteServiceWorkerAssetsManifest" + DependsOnTargets="_ComputeServiceWorkerAssets;ResolveStaticWebAssetsInputs"> + + <ItemGroup> + <_ServiceWorkItem Include="@(StaticWebAsset)" Exclude="$(_ServiceWorkerAssetsManifestFullPath);@(_ServiceWorkerStaticWebAsset)"> + <AssetUrl>$([System.String]::Copy('$([System.String]::Copy('%(StaticWebAsset.BasePath)').TrimEnd('/'))/%(StaticWebAsset.RelativePath)').Replace('\','/').TrimStart('/'))</AssetUrl> + </_ServiceWorkItem> + </ItemGroup> + + <GenerateServiceWorkerAssetsManifest + Version="$(ServiceWorkerAssetsManifestVersion)" + Assets="@(_ServiceWorkItem)" + OutputPath="$(_ServiceWorkerAssetsManifestIntermediateOutputPath)"> + <Output TaskParameter="CalculatedVersion" PropertyName="_ServiceWorkerAssetsManifestVersion" /> + </GenerateServiceWorkerAssetsManifest> + + <Copy + SourceFiles="%(_ServiceWorkerIntermediateFile.ContentSourcePath)" + DestinationFiles="%(_ServiceWorkerIntermediateFile.Identity)" /> + + <WriteLinesToFile + File="%(_ServiceWorkerIntermediateFile.Identity)" + Lines="/* Manifest version: $(_ServiceWorkerAssetsManifestVersion) */" + Condition="'$(_ServiceWorkerAssetsManifestVersion)' != ''" /> + + <ItemGroup> + <FileWrites Include="@(_ServiceWorkerIntermediateFile)" /> + <FileWrites Include="$(_ServiceWorkerAssetsManifestIntermediateOutputPath)" /> + </ItemGroup> + + </Target> + + <Target Name="_BlazorStaticAssetsCopyFilesToOutputDirectory" AfterTargets="CopyFilesToOutputDirectory" DependsOnTargets="_WriteServiceWorkerAssetsManifest"> + <Copy + SourceFiles="@(_ManifestStaticWebAsset);@(_ServiceWorkerStaticWebAsset)" + DestinationFiles="$(OutDir)wwwroot\%(RelativePath)" + SkipUnchangedFiles="$(SkipCopyUnchangedFiles)" + OverwriteReadOnlyFiles="$(OverwriteReadOnlyFiles)" + Retries="$(CopyRetryCount)" + RetryDelayMilliseconds="$(CopyRetryDelayMilliseconds)" + UseHardlinksIfPossible="$(CreateHardLinksForCopyFilesToOutputDirectoryIfPossible)" + UseSymboliclinksIfPossible="$(CreateSymbolicLinksForCopyFilesToOutputDirectoryIfPossible)" + ErrorIfLinkFails="$(ErrorIfLinkFailsForCopyFilesToOutputDirectory)" + Condition="Exists('%(Identity)')"> + + <Output TaskParameter="DestinationFiles" ItemName="FileWrites"/> + </Copy> + </Target> + + <Target Name="_OmitServiceWorkerContent" + BeforeTargets="AssignTargetPaths;ResolveCurrentProjectStaticWebAssetsInputs"> + + <ItemGroup> + <!-- Don't emit the service worker source files to the output --> + <Content Remove="@(ServiceWorker)" /> + <Content Remove="@(ServiceWorker->'%(PublishedContent)')" /> + </ItemGroup> + </Target> + + <Target Name="_GenerateServiceWorkerFileForPublish" + BeforeTargets="_BlazorCompressPublishFiles" + AfterTargets="_ProcessPublishFilesForBlazor"> + + <PropertyGroup> + <_ServiceWorkerAssetsManifestPublishIntermediateOutputPath>$(IntermediateOutputPath)publish-$(ServiceWorkerAssetsManifest)</_ServiceWorkerAssetsManifestPublishIntermediateOutputPath> + </PropertyGroup> + + <ItemGroup> + <_ServiceWorkerIntermediatePublishFile Include="$(IntermediateOutputPath)serviceworkers\%(FileName).publish%(Extension)"> + <ContentSourcePath Condition="'%(ServiceWorker.PublishedContent)' != ''">%(ServiceWorker.PublishedContent)</ContentSourcePath> + <ContentSourcePath Condition="'%(ServiceWorker.PublishedContent)' == ''">%(ServiceWorker.Identity)</ContentSourcePath> + <RelativePath>%(ServiceWorker.Identity)</RelativePath> + </_ServiceWorkerIntermediatePublishFile> + + <_ServiceWorkerPublishFile Include="@(ResolvedFileToPublish)" Condition="$([System.String]::Copy('%(ResolvedFileToPublish.RelativePath)').Replace('\','/').StartsWith('wwwroot/'))"> + <AssetUrl>$([System.String]::Copy('%(ResolvedFileToPublish.RelativePath)').Replace('\','/').TrimStart('/'))</AssetUrl> + <AssetUrl>$([System.String]::Copy('%(RelativePath)').Replace('\','/').Substring(8))</AssetUrl> + </_ServiceWorkerPublishFile> + </ItemGroup> + + <GenerateServiceWorkerAssetsManifest + Version="$(ServiceWorkerAssetsManifestVersion)" + Assets="@(_ServiceWorkerPublishFile)" + OutputPath="$(_ServiceWorkerAssetsManifestPublishIntermediateOutputPath)"> + + <Output TaskParameter="CalculatedVersion" PropertyName="_ServiceWorkerPublishAssetsManifestVersion" /> + </GenerateServiceWorkerAssetsManifest> + + <Copy SourceFiles="%(_ServiceWorkerIntermediatePublishFile.ContentSourcePath)" + DestinationFiles="%(_ServiceWorkerIntermediatePublishFile.Identity)" /> + + <WriteLinesToFile + File="%(_ServiceWorkerIntermediatePublishFile.Identity)" + Lines="/* Manifest version: $(_ServiceWorkerPublishAssetsManifestVersion) */" /> + + <ItemGroup> + <ResolvedFileToPublish + Include="@(_ServiceWorkerIntermediatePublishFile)" + CopyToPublishDirectory="PreserveNewest" + RelativePath="%(_ServiceWorkerIntermediatePublishFile.RelativePath)" + ExcludeFromSingleFile="true" /> + + <ResolvedFileToPublish + Include="$(_ServiceWorkerAssetsManifestPublishIntermediateOutputPath)" + CopyToPublishDirectory="PreserveNewest" + RelativePath="wwwroot\$(ServiceWorkerAssetsManifest)" + ExcludeFromSingleFile="true" /> + </ItemGroup> + </Target> + +</Project> diff --git a/src/Components/WebAssembly/Sdk/test/BlazorReadSatelliteAssemblyFileTest.cs b/src/Components/WebAssembly/Sdk/test/BlazorReadSatelliteAssemblyFileTest.cs new file mode 100644 index 0000000000000000000000000000000000000000..b95a6154b663fcc5526c72c0243479a06560234b --- /dev/null +++ b/src/Components/WebAssembly/Sdk/test/BlazorReadSatelliteAssemblyFileTest.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.Collections.Generic; +using System.IO; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Moq; +using Xunit; + +namespace Microsoft.NET.Sdk.BlazorWebAssembly +{ + public class BlazorReadSatelliteAssemblyFileTest + { + [Fact] + public void WritesAndReadsRoundTrip() + { + // Arrange/Act + var tempFile = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + + var writer = new BlazorWriteSatelliteAssemblyFile + { + BuildEngine = Mock.Of<IBuildEngine>(), + WriteFile = new TaskItem(tempFile), + SatelliteAssembly = new[] + { + new TaskItem("Resources.fr.dll", new Dictionary<string, string> + { + ["Culture"] = "fr", + ["DestinationSubDirectory"] = "fr\\", + }), + new TaskItem("Resources.ja-jp.dll", new Dictionary<string, string> + { + ["Culture"] = "ja-jp", + ["DestinationSubDirectory"] = "ja-jp\\", + }), + }, + }; + + var reader = new BlazorReadSatelliteAssemblyFile + { + BuildEngine = Mock.Of<IBuildEngine>(), + ReadFile = new TaskItem(tempFile), + }; + + writer.Execute(); + + Assert.True(File.Exists(tempFile), "Write should have succeeded."); + + reader.Execute(); + + Assert.Collection( + reader.SatelliteAssembly, + assembly => + { + Assert.Equal("Resources.fr.dll", assembly.ItemSpec); + Assert.Equal("fr", assembly.GetMetadata("Culture")); + Assert.Equal("fr\\", assembly.GetMetadata("DestinationSubDirectory")); + }, + assembly => + { + Assert.Equal("Resources.ja-jp.dll", assembly.ItemSpec); + Assert.Equal("ja-jp", assembly.GetMetadata("Culture")); + Assert.Equal("ja-jp\\", assembly.GetMetadata("DestinationSubDirectory")); + }); + } + } +} diff --git a/src/Components/WebAssembly/Sdk/test/GenerateBlazorBootJsonTest.cs b/src/Components/WebAssembly/Sdk/test/GenerateBlazorBootJsonTest.cs new file mode 100644 index 0000000000000000000000000000000000000000..139f22f27fbc42d4d7b8d42c32ce153680f161b7 --- /dev/null +++ b/src/Components/WebAssembly/Sdk/test/GenerateBlazorBootJsonTest.cs @@ -0,0 +1,195 @@ +// 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.Linq; +using System.Runtime.Serialization.Json; +using Microsoft.Build.Framework; +using Moq; +using Xunit; + +namespace Microsoft.NET.Sdk.BlazorWebAssembly +{ + public class GenerateBlazorWebAssemblyBootJsonTest + { + [Fact] + public void GroupsResourcesByType() + { + // Arrange + var taskInstance = new GenerateBlazorWebAssemblyBootJson + { + AssemblyPath = "MyApp.Entrypoint.dll", + Resources = new[] + { + CreateResourceTaskItem( + ("FileName", "My.Assembly1"), + ("Extension", ".dll"), + ("FileHash", "abcdefghikjlmnopqrstuvwxyz")), + + CreateResourceTaskItem( + ("FileName", "My.Assembly2"), + ("Extension", ".dll"), + ("FileHash", "012345678901234567890123456789")), + + CreateResourceTaskItem( + ("FileName", "SomePdb"), + ("Extension", ".pdb"), + ("FileHash", "pdbhashpdbhashpdbhash")), + + CreateResourceTaskItem( + ("FileName", "My.Assembly1"), + ("Extension", ".pdb"), + ("FileHash", "pdbdefghikjlmnopqrstuvwxyz")), + + CreateResourceTaskItem( + ("FileName", "some-runtime-file"), + ("FileHash", "runtimehashruntimehash"), + ("AssetType", "native")), + + CreateResourceTaskItem( + ("FileName", "satellite-assembly1"), + ("Extension", ".dll"), + ("FileHash", "hashsatelliteassembly1"), + ("Culture", "en-GB")), + + CreateResourceTaskItem( + ("FileName", "satellite-assembly2"), + ("Extension", ".dll"), + ("FileHash", "hashsatelliteassembly2"), + ("Culture", "fr")), + + CreateResourceTaskItem( + ("FileName", "satellite-assembly3"), + ("Extension", ".dll"), + ("FileHash", "hashsatelliteassembly3"), + ("Culture", "en-GB")), + } + }; + + using var stream = new MemoryStream(); + + // Act + taskInstance.WriteBootJson(stream, "MyEntrypointAssembly"); + + // Assert + var parsedContent = ParseBootData(stream); + Assert.Equal("MyEntrypointAssembly", parsedContent.entryAssembly); + + var resources = parsedContent.resources.assembly; + Assert.Equal(2, resources.Count); + Assert.Equal("sha256-abcdefghikjlmnopqrstuvwxyz", resources["My.Assembly1.dll"]); + Assert.Equal("sha256-012345678901234567890123456789", resources["My.Assembly2.dll"]); + + resources = parsedContent.resources.pdb; + Assert.Equal(2, resources.Count); + Assert.Equal("sha256-pdbhashpdbhashpdbhash", resources["SomePdb.pdb"]); + Assert.Equal("sha256-pdbdefghikjlmnopqrstuvwxyz", resources["My.Assembly1.pdb"]); + + resources = parsedContent.resources.runtime; + Assert.Single(resources); + Assert.Equal("sha256-runtimehashruntimehash", resources["some-runtime-file"]); + + var satelliteResources = parsedContent.resources.satelliteResources; + Assert.Collection( + satelliteResources.OrderBy(kvp => kvp.Key), + kvp => + { + Assert.Equal("en-GB", kvp.Key); + Assert.Collection( + kvp.Value.OrderBy(item => item.Key), + item => + { + Assert.Equal("en-GB/satellite-assembly1.dll", item.Key); + Assert.Equal("sha256-hashsatelliteassembly1", item.Value); + }, + item => + { + Assert.Equal("en-GB/satellite-assembly3.dll", item.Key); + Assert.Equal("sha256-hashsatelliteassembly3", item.Value); + }); + }, + kvp => + { + Assert.Equal("fr", kvp.Key); + Assert.Collection( + kvp.Value.OrderBy(item => item.Key), + item => + { + Assert.Equal("fr/satellite-assembly2.dll", item.Key); + Assert.Equal("sha256-hashsatelliteassembly2", item.Value); + }); + }); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void CanSpecifyCacheBootResources(bool flagValue) + { + // Arrange + var taskInstance = new GenerateBlazorWebAssemblyBootJson { CacheBootResources = flagValue }; + using var stream = new MemoryStream(); + + // Act + taskInstance.WriteBootJson(stream, "MyEntrypointAssembly"); + + // Assert + var parsedContent = ParseBootData(stream); + Assert.Equal(flagValue, parsedContent.cacheBootResources); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void CanSpecifyDebugBuild(bool flagValue) + { + // Arrange + var taskInstance = new GenerateBlazorWebAssemblyBootJson { DebugBuild = flagValue }; + using var stream = new MemoryStream(); + + // Act + taskInstance.WriteBootJson(stream, "MyEntrypointAssembly"); + + // Assert + var parsedContent = ParseBootData(stream); + Assert.Equal(flagValue, parsedContent.debugBuild); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void CanSpecifyLinkerEnabled(bool flagValue) + { + // Arrange + var taskInstance = new GenerateBlazorWebAssemblyBootJson { LinkerEnabled = flagValue }; + using var stream = new MemoryStream(); + + // Act + taskInstance.WriteBootJson(stream, "MyEntrypointAssembly"); + + // Assert + var parsedContent = ParseBootData(stream); + Assert.Equal(flagValue, parsedContent.linkerEnabled); + } + + private static BootJsonData ParseBootData(Stream stream) + { + stream.Position = 0; + var serializer = new DataContractJsonSerializer( + typeof(BootJsonData), + new DataContractJsonSerializerSettings { UseSimpleDictionaryFormat = true }); + return (BootJsonData)serializer.ReadObject(stream); + } + + private static ITaskItem CreateResourceTaskItem(params (string key, string value)[] values) + { + var mock = new Mock<ITaskItem>(); + + foreach (var (key, value) in values) + { + mock.Setup(m => m.GetMetadata(key)).Returns(value); + } + return mock.Object; + } + } +} diff --git a/src/Components/WebAssembly/Sdk/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests.csproj b/src/Components/WebAssembly/Sdk/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests.csproj new file mode 100644 index 0000000000000000000000000000000000000000..5d30b4f7814a743b872fb82eada24d1b159cb60c --- /dev/null +++ b/src/Components/WebAssembly/Sdk/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests.csproj @@ -0,0 +1,12 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework> + </PropertyGroup> + + <ItemGroup> + <Reference Include="Microsoft.Build.Utilities.Core" /> + <Reference Include="Microsoft.NET.Sdk.BlazorWebAssembly" /> + </ItemGroup> + +</Project> diff --git a/src/Components/WebAssembly/Sdk/testassets/Directory.Build.props b/src/Components/WebAssembly/Sdk/testassets/Directory.Build.props new file mode 100644 index 0000000000000000000000000000000000000000..6d0949542f9f929e151c02e2f1c93d1e13a639e2 --- /dev/null +++ b/src/Components/WebAssembly/Sdk/testassets/Directory.Build.props @@ -0,0 +1,50 @@ +<Project> + <Import Project="Before.Directory.Build.props" Condition="Exists('Before.Directory.Build.props')" /> + + <PropertyGroup> + <!-- + In the case that a user is building a sample directly the MicrosoftNetCompilersToolsetPackagerVersion will not be provided. + We'll fall back to whatever the current SDK provides in regards to Roslyn's Microsoft.Net.Compilers.Toolset. + --> + <BuildingTestAppsIndependently>false</BuildingTestAppsIndependently> + <BuildingTestAppsIndependently Condition="'$(MicrosoftNetCompilersToolsetPackageVersion)' == ''">true</BuildingTestAppsIndependently> + + <!-- Do not resolve Reference ItemGroup since it has a different semantic meaning in Razor test apps --> + <EnableCustomReferenceResolution>false</EnableCustomReferenceResolution> + + <DefaultNetCoreTargetFramework>net5.0</DefaultNetCoreTargetFramework> + + <RepoRoot Condition="'$(RepoRoot)' == ''">$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory)..\, 'AspNetCore.sln'))\</RepoRoot> + + <RazorSdkCurrentVersionProps>$(RepoRoot)src\Razor\Microsoft.NET.Sdk.Razor\src\build\netstandard2.0\Sdk.Razor.CurrentVersion.props</RazorSdkCurrentVersionProps> + <RazorSdkCurrentVersionTargets>$(RepoRoot)src\Razor\Microsoft.NET.Sdk.Razor\src\build\netstandard2.0\Sdk.Razor.CurrentVersion.targets</RazorSdkCurrentVersionTargets> + <RazorSdkArtifactsDirectory>$(RepoRoot)artifacts\bin\Microsoft.NET.Sdk.Razor\</RazorSdkArtifactsDirectory> + <BlazorWebAssemblySdkArtifactsDirectory>$(RepoRoot)artifacts\bin\Microsoft.NET.Sdk.BlazorWebAssembly\</BlazorWebAssemblySdkArtifactsDirectory> + <BlazorWebAssemblyJSPath>$(MSBuildThisFileDirectory)blazor.webassembly.js</BlazorWebAssemblyJSPath> + </PropertyGroup> + + <Import Project="$(RepoRoot)eng\Versions.props" /> + + <PropertyGroup> + <!-- Reset version prefix to 1.0.0 for test projects --> + <VersionPrefix>1.0.0</VersionPrefix> + + <!-- Turn down the compression level for brotli --> + <_BlazorBrotliCompressionLevel>NoCompression</_BlazorBrotliCompressionLevel> + </PropertyGroup> + + <ItemGroup> + <!-- Have the SDK treat the MvcShim as an MVC assembly --> + <_MvcAssemblyName Include="Microsoft.AspNetCore.Razor.Test.MvcShim.ClassLib" /> + </ItemGroup> + + <ItemGroup Condition="$(BuildingTestAppsIndependently) == 'false'"> + <PackageReference Include="Microsoft.Net.Compilers.Toolset" + Version="$(MicrosoftNetCompilersToolsetPackageVersion)" + PrivateAssets="all" + IsImplicitlyDefined="true" /> + </ItemGroup> + + <Import Project="After.Directory.Build.props" Condition="Exists('After.Directory.Build.props')" /> + +</Project> diff --git a/src/Components/WebAssembly/Sdk/testassets/Directory.Build.targets b/src/Components/WebAssembly/Sdk/testassets/Directory.Build.targets new file mode 100644 index 0000000000000000000000000000000000000000..45a2ee1e5650483a3e9ba65f5dc9e2b68edbba03 --- /dev/null +++ b/src/Components/WebAssembly/Sdk/testassets/Directory.Build.targets @@ -0,0 +1,7 @@ +<Project> + <PropertyGroup> + <RuntimeFrameworkVersion Condition=" '$(TargetFramework)' == '$(DefaultNetCoreTargetFramework)' ">$(MicrosoftNETCoreAppRuntimeVersion)</RuntimeFrameworkVersion> + <!-- aspnet/BuildTools#662 Don't police what version of NetCoreApp we use --> + <NETCoreAppMaximumVersion>99.9</NETCoreAppMaximumVersion> + </PropertyGroup> +</Project> diff --git a/src/Razor/test/testassets/LinkBaseToWebRoot/js/LinkedScript.js b/src/Components/WebAssembly/Sdk/testassets/LinkBaseToWebRoot/js/LinkedScript.js similarity index 100% rename from src/Razor/test/testassets/LinkBaseToWebRoot/js/LinkedScript.js rename to src/Components/WebAssembly/Sdk/testassets/LinkBaseToWebRoot/js/LinkedScript.js diff --git a/src/Components/WebAssembly/Sdk/testassets/RestoreBlazorWasmTestProjects/RestoreBlazorWasmTestProjects.csproj b/src/Components/WebAssembly/Sdk/testassets/RestoreBlazorWasmTestProjects/RestoreBlazorWasmTestProjects.csproj new file mode 100644 index 0000000000000000000000000000000000000000..f4debf808803f70899bfbfed78c6c896f0b54929 --- /dev/null +++ b/src/Components/WebAssembly/Sdk/testassets/RestoreBlazorWasmTestProjects/RestoreBlazorWasmTestProjects.csproj @@ -0,0 +1,12 @@ +<Project Sdk="Microsoft.NET.Sdk"> + <PropertyGroup> + <TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework> + </PropertyGroup> + + <ItemGroup> + <ProjectReference Include="..\blazorhosted\blazorhosted.csproj" /> + <ProjectReference Include="..\blazorhosted-rid\blazorhosted-rid.csproj" /> + <ProjectReference Include="..\blazorwasm\blazorwasm.csproj" /> + </ItemGroup> + +</Project> diff --git a/src/Components/WebAssembly/Sdk/testassets/blazor.webassembly.js b/src/Components/WebAssembly/Sdk/testassets/blazor.webassembly.js new file mode 100644 index 0000000000000000000000000000000000000000..84362ca046b156d71d84fa5356ae9afbcb282919 --- /dev/null +++ b/src/Components/WebAssembly/Sdk/testassets/blazor.webassembly.js @@ -0,0 +1 @@ +Test file \ No newline at end of file diff --git a/src/Razor/test/testassets/blazorhosted-rid/Program.cs b/src/Components/WebAssembly/Sdk/testassets/blazorhosted-rid/Program.cs similarity index 100% rename from src/Razor/test/testassets/blazorhosted-rid/Program.cs rename to src/Components/WebAssembly/Sdk/testassets/blazorhosted-rid/Program.cs diff --git a/src/Razor/test/testassets/blazorhosted-rid/blazorhosted-rid.csproj b/src/Components/WebAssembly/Sdk/testassets/blazorhosted-rid/blazorhosted-rid.csproj similarity index 100% rename from src/Razor/test/testassets/blazorhosted-rid/blazorhosted-rid.csproj rename to src/Components/WebAssembly/Sdk/testassets/blazorhosted-rid/blazorhosted-rid.csproj diff --git a/src/Razor/test/testassets/blazorhosted/Program.cs b/src/Components/WebAssembly/Sdk/testassets/blazorhosted/Program.cs similarity index 100% rename from src/Razor/test/testassets/blazorhosted/Program.cs rename to src/Components/WebAssembly/Sdk/testassets/blazorhosted/Program.cs diff --git a/src/Razor/test/testassets/blazorhosted/blazorhosted.csproj b/src/Components/WebAssembly/Sdk/testassets/blazorhosted/blazorhosted.csproj similarity index 100% rename from src/Razor/test/testassets/blazorhosted/blazorhosted.csproj rename to src/Components/WebAssembly/Sdk/testassets/blazorhosted/blazorhosted.csproj diff --git a/src/Razor/test/testassets/blazorwasm/App.razor b/src/Components/WebAssembly/Sdk/testassets/blazorwasm/App.razor similarity index 100% rename from src/Razor/test/testassets/blazorwasm/App.razor rename to src/Components/WebAssembly/Sdk/testassets/blazorwasm/App.razor diff --git a/src/Razor/test/testassets/blazorwasm/LinkToWebRoot/css/app.css b/src/Components/WebAssembly/Sdk/testassets/blazorwasm/LinkToWebRoot/css/app.css similarity index 100% rename from src/Razor/test/testassets/blazorwasm/LinkToWebRoot/css/app.css rename to src/Components/WebAssembly/Sdk/testassets/blazorwasm/LinkToWebRoot/css/app.css diff --git a/src/Razor/test/testassets/blazorwasm/Pages/Index.razor b/src/Components/WebAssembly/Sdk/testassets/blazorwasm/Pages/Index.razor similarity index 100% rename from src/Razor/test/testassets/blazorwasm/Pages/Index.razor rename to src/Components/WebAssembly/Sdk/testassets/blazorwasm/Pages/Index.razor diff --git a/src/Razor/test/testassets/blazorwasm/Program.cs b/src/Components/WebAssembly/Sdk/testassets/blazorwasm/Program.cs similarity index 100% rename from src/Razor/test/testassets/blazorwasm/Program.cs rename to src/Components/WebAssembly/Sdk/testassets/blazorwasm/Program.cs diff --git a/src/Razor/test/testassets/blazorwasm/Resources.ja.resx.txt b/src/Components/WebAssembly/Sdk/testassets/blazorwasm/Resources.ja.resx.txt similarity index 100% rename from src/Razor/test/testassets/blazorwasm/Resources.ja.resx.txt rename to src/Components/WebAssembly/Sdk/testassets/blazorwasm/Resources.ja.resx.txt diff --git a/src/Razor/test/testassets/blazorwasm/_Imports.razor b/src/Components/WebAssembly/Sdk/testassets/blazorwasm/_Imports.razor similarity index 100% rename from src/Razor/test/testassets/blazorwasm/_Imports.razor rename to src/Components/WebAssembly/Sdk/testassets/blazorwasm/_Imports.razor diff --git a/src/Razor/test/testassets/blazorwasm/blazorwasm.csproj b/src/Components/WebAssembly/Sdk/testassets/blazorwasm/blazorwasm.csproj similarity index 88% rename from src/Razor/test/testassets/blazorwasm/blazorwasm.csproj rename to src/Components/WebAssembly/Sdk/testassets/blazorwasm/blazorwasm.csproj index 36a511e7c77fa6d890c9953eb542c7db61b8b5e4..c8906b1bd290604f6ba88d752a6cb8f7e56f6edc 100644 --- a/src/Razor/test/testassets/blazorwasm/blazorwasm.csproj +++ b/src/Components/WebAssembly/Sdk/testassets/blazorwasm/blazorwasm.csproj @@ -1,10 +1,12 @@ <Project Sdk="Microsoft.NET.Sdk.Razor"> + + <Import Project="$(RepoRoot)src\Components\WebAssembly\Sdk\src\Sdk\Sdk.props" /> + <PropertyGroup> <TargetFramework>net5.0</TargetFramework> - <UseBlazorWebAssembly>true</UseBlazorWebAssembly> <RuntimeIdentifier>browser-wasm</RuntimeIdentifier> - <AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath> <RazorSdkDirectoryRoot>$(RazorSdkArtifactsDirectory)$(Configuration)\sdk-output\</RazorSdkDirectoryRoot> + <BlazorWebAssemblySdkDirectoryRoot>$(BlazorWebAssemblySdkArtifactsDirectory)$(Configuration)\sdk-output\</BlazorWebAssemblySdkDirectoryRoot> <ServiceWorkerAssetsManifest>custom-service-worker-assets.js</ServiceWorkerAssetsManifest> </PropertyGroup> @@ -50,4 +52,6 @@ <ServiceWorker Include="wwwroot\serviceworkers\my-service-worker.js" PublishedContent="wwwroot\serviceworkers\my-prod-service-worker.js" /> </ItemGroup> + <Import Project="$(RepoRoot)src\Components\WebAssembly\Sdk\src\Sdk\Sdk.targets" /> + </Project> diff --git a/src/Razor/test/testassets/blazorwasm/wwwroot/Fake-License.txt b/src/Components/WebAssembly/Sdk/testassets/blazorwasm/wwwroot/Fake-License.txt similarity index 100% rename from src/Razor/test/testassets/blazorwasm/wwwroot/Fake-License.txt rename to src/Components/WebAssembly/Sdk/testassets/blazorwasm/wwwroot/Fake-License.txt diff --git a/src/Razor/test/testassets/blazorwasm/wwwroot/css/app.css b/src/Components/WebAssembly/Sdk/testassets/blazorwasm/wwwroot/css/app.css similarity index 100% rename from src/Razor/test/testassets/blazorwasm/wwwroot/css/app.css rename to src/Components/WebAssembly/Sdk/testassets/blazorwasm/wwwroot/css/app.css diff --git a/src/Razor/test/testassets/blazorwasm/wwwroot/index.html b/src/Components/WebAssembly/Sdk/testassets/blazorwasm/wwwroot/index.html similarity index 100% rename from src/Razor/test/testassets/blazorwasm/wwwroot/index.html rename to src/Components/WebAssembly/Sdk/testassets/blazorwasm/wwwroot/index.html diff --git a/src/Razor/test/testassets/blazorwasm/wwwroot/serviceworkers/my-prod-service-worker.js b/src/Components/WebAssembly/Sdk/testassets/blazorwasm/wwwroot/serviceworkers/my-prod-service-worker.js similarity index 100% rename from src/Razor/test/testassets/blazorwasm/wwwroot/serviceworkers/my-prod-service-worker.js rename to src/Components/WebAssembly/Sdk/testassets/blazorwasm/wwwroot/serviceworkers/my-prod-service-worker.js diff --git a/src/Razor/test/testassets/blazorwasm/wwwroot/serviceworkers/my-service-worker.js b/src/Components/WebAssembly/Sdk/testassets/blazorwasm/wwwroot/serviceworkers/my-service-worker.js similarity index 100% rename from src/Razor/test/testassets/blazorwasm/wwwroot/serviceworkers/my-service-worker.js rename to src/Components/WebAssembly/Sdk/testassets/blazorwasm/wwwroot/serviceworkers/my-service-worker.js diff --git a/src/Razor/test/testassets/classlibrarywithsatelliteassemblies/Class1.cs b/src/Components/WebAssembly/Sdk/testassets/classlibrarywithsatelliteassemblies/Class1.cs similarity index 100% rename from src/Razor/test/testassets/classlibrarywithsatelliteassemblies/Class1.cs rename to src/Components/WebAssembly/Sdk/testassets/classlibrarywithsatelliteassemblies/Class1.cs diff --git a/src/Razor/test/testassets/classlibrarywithsatelliteassemblies/Resources.es-ES.resx b/src/Components/WebAssembly/Sdk/testassets/classlibrarywithsatelliteassemblies/Resources.es-ES.resx similarity index 100% rename from src/Razor/test/testassets/classlibrarywithsatelliteassemblies/Resources.es-ES.resx rename to src/Components/WebAssembly/Sdk/testassets/classlibrarywithsatelliteassemblies/Resources.es-ES.resx diff --git a/src/Razor/test/testassets/classlibrarywithsatelliteassemblies/classlibrarywithsatelliteassemblies.csproj b/src/Components/WebAssembly/Sdk/testassets/classlibrarywithsatelliteassemblies/classlibrarywithsatelliteassemblies.csproj similarity index 100% rename from src/Razor/test/testassets/classlibrarywithsatelliteassemblies/classlibrarywithsatelliteassemblies.csproj rename to src/Components/WebAssembly/Sdk/testassets/classlibrarywithsatelliteassemblies/classlibrarywithsatelliteassemblies.csproj diff --git a/src/Razor/test/testassets/razorclasslibrary/Class1.cs b/src/Components/WebAssembly/Sdk/testassets/razorclasslibrary/Class1.cs similarity index 100% rename from src/Razor/test/testassets/razorclasslibrary/Class1.cs rename to src/Components/WebAssembly/Sdk/testassets/razorclasslibrary/Class1.cs diff --git a/src/Razor/test/testassets/razorclasslibrary/RazorClassLibrary.csproj b/src/Components/WebAssembly/Sdk/testassets/razorclasslibrary/RazorClassLibrary.csproj similarity index 100% rename from src/Razor/test/testassets/razorclasslibrary/RazorClassLibrary.csproj rename to src/Components/WebAssembly/Sdk/testassets/razorclasslibrary/RazorClassLibrary.csproj diff --git a/src/Razor/test/testassets/razorclasslibrary/wwwroot/styles.css b/src/Components/WebAssembly/Sdk/testassets/razorclasslibrary/wwwroot/styles.css similarity index 100% rename from src/Razor/test/testassets/razorclasslibrary/wwwroot/styles.css rename to src/Components/WebAssembly/Sdk/testassets/razorclasslibrary/wwwroot/styles.css diff --git a/src/Components/WebAssembly/Sdk/testassets/razorclasslibrary/wwwroot/wwwroot/exampleJsInterop.js b/src/Components/WebAssembly/Sdk/testassets/razorclasslibrary/wwwroot/wwwroot/exampleJsInterop.js new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/Components/WebAssembly/Sdk/tools/Application.cs b/src/Components/WebAssembly/Sdk/tools/Application.cs new file mode 100644 index 0000000000000000000000000000000000000000..d9d66d10bc8b678e425b9af5a82628b272e9ea96 --- /dev/null +++ b/src/Components/WebAssembly/Sdk/tools/Application.cs @@ -0,0 +1,98 @@ +// 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.Reflection; +using System.Threading; +using Microsoft.Extensions.CommandLineUtils; + +namespace Microsoft.NET.Sdk.BlazorWebAssembly.Tools +{ + internal class Application : CommandLineApplication + { + public Application( + CancellationToken cancellationToken, + TextWriter output = null, + TextWriter error = null) + { + CancellationToken = cancellationToken; + Out = output ?? Out; + Error = error ?? Error; + + Name = "BlazorWebAssembly.Tools"; + FullName = "Microsoft Blazor WebAssembly SDK tool"; + Description = "CLI for Blazor WebAssembly operations."; + ShortVersionGetter = GetInformationalVersion; + + HelpOption("-?|-h|--help"); + + Commands.Add(new BrotliCompressCommand(this)); + } + + public CancellationToken CancellationToken { get; } + + public new int Execute(params string[] args) + { + try + { + return base.Execute(ExpandResponseFiles(args)); + } + catch (AggregateException ex) when (ex.InnerException != null) + { + foreach (var innerException in ex.Flatten().InnerExceptions) + { + Error.WriteLine(innerException.Message); + Error.WriteLine(innerException.StackTrace); + } + return 1; + } + catch (CommandParsingException ex) + { + // Don't show a call stack when we have unneeded arguments, just print the error message. + // The code that throws this exception will print help, so no need to do it here. + Error.WriteLine(ex.Message); + return 1; + } + catch (OperationCanceledException) + { + // This is a cancellation, not a failure. + Error.WriteLine("Cancelled"); + return 1; + } + catch (Exception ex) + { + Error.WriteLine(ex.Message); + Error.WriteLine(ex.StackTrace); + return 1; + } + } + + private string GetInformationalVersion() + { + var assembly = typeof(Application).GetTypeInfo().Assembly; + var attribute = assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>(); + return attribute.InformationalVersion; + } + + private static string[] ExpandResponseFiles(string[] args) + { + var expandedArgs = new List<string>(); + foreach (var arg in args) + { + if (!arg.StartsWith("@", StringComparison.Ordinal)) + { + expandedArgs.Add(arg); + } + else + { + var fileName = arg.Substring(1); + expandedArgs.AddRange(File.ReadLines(fileName)); + } + } + + return expandedArgs.ToArray(); + } + } +} \ No newline at end of file diff --git a/src/Components/WebAssembly/Sdk/tools/BrotliCompressCommand.cs b/src/Components/WebAssembly/Sdk/tools/BrotliCompressCommand.cs new file mode 100644 index 0000000000000000000000000000000000000000..136c2fc660eee25617d528ca1745428c766e126b --- /dev/null +++ b/src/Components/WebAssembly/Sdk/tools/BrotliCompressCommand.cs @@ -0,0 +1,85 @@ +// 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.IO.Compression; +using System.Threading.Tasks; +using Microsoft.Extensions.CommandLineUtils; + +namespace Microsoft.NET.Sdk.BlazorWebAssembly.Tools +{ + internal class BrotliCompressCommand : CommandLineApplication + { + public BrotliCompressCommand(Application parent) + : base(throwOnUnexpectedArg: true) + { + base.Parent = parent; + Name = "brotli"; + Sources = Option("-s", "files to compress", CommandOptionType.MultipleValue); + Outputs = Option("-o", "Output file path", CommandOptionType.MultipleValue); + CompressionLevelOption = Option("-c", "Compression level", CommandOptionType.SingleValue); + + Invoke = () => Execute().GetAwaiter().GetResult(); + } + + public CommandOption Sources { get; } + + public CommandOption Outputs { get; } + + public CommandOption CompressionLevelOption { get; } + + public CompressionLevel CompressionLevel { get; private set; } = CompressionLevel.Optimal; + + private Task<int> Execute() + { + if (!ValidateArguments()) + { + ShowHelp(); + return Task.FromResult(1); + } + + return ExecuteCoreAsync(); + } + + private bool ValidateArguments() + { + if (Sources.Values.Count != Outputs.Values.Count) + { + Error.WriteLine($"{Sources.Description} has {Sources.Values.Count}, but {Outputs.Description} has {Outputs.Values.Count} values."); + return false; + } + + if (CompressionLevelOption.HasValue()) + { + if (!Enum.TryParse<CompressionLevel>(CompressionLevelOption.Value(), out var value)) + { + Error.WriteLine($"Invalid option {CompressionLevelOption.Value()} for {CompressionLevelOption.Template}."); + return false; + } + + CompressionLevel = value; + } + + return true; + } + + private Task<int> ExecuteCoreAsync() + { + Parallel.For(0, Sources.Values.Count, i => + { + var source = Sources.Values[i]; + var output = Outputs.Values[i]; + + using var sourceStream = File.OpenRead(source); + using var fileStream = new FileStream(output, FileMode.Create); + + using var stream = new BrotliStream(fileStream, CompressionLevel); + + sourceStream.CopyTo(stream); + }); + + return Task.FromResult(0); + } + } +} diff --git a/src/Components/WebAssembly/Sdk/tools/DebugMode.cs b/src/Components/WebAssembly/Sdk/tools/DebugMode.cs new file mode 100644 index 0000000000000000000000000000000000000000..816bb4a783555cc75e835592bc3d57a406e41ed7 --- /dev/null +++ b/src/Components/WebAssembly/Sdk/tools/DebugMode.cs @@ -0,0 +1,27 @@ +// 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.Threading; + +namespace Microsoft.NET.Sdk.BlazorWebAssembly.Tools +{ + internal static class DebugMode + { + public static void HandleDebugSwitch(ref string[] args) + { + if (args.Length > 0 && string.Equals("--debug", args[0], StringComparison.OrdinalIgnoreCase)) + { + args = args.Skip(1).ToArray(); + + Console.WriteLine("Waiting for debugger in pid: {0}", Process.GetCurrentProcess().Id); + while (!Debugger.IsAttached) + { + Thread.Sleep(TimeSpan.FromSeconds(3)); + } + } + } + } +} \ No newline at end of file diff --git a/src/Components/WebAssembly/Sdk/tools/Microsoft.NET.Sdk.BlazorWebAssembly.Tools.csproj b/src/Components/WebAssembly/Sdk/tools/Microsoft.NET.Sdk.BlazorWebAssembly.Tools.csproj new file mode 100644 index 0000000000000000000000000000000000000000..6ac7a26c1924308fabd759e1b519b60ef12524ea --- /dev/null +++ b/src/Components/WebAssembly/Sdk/tools/Microsoft.NET.Sdk.BlazorWebAssembly.Tools.csproj @@ -0,0 +1,22 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <OutputType>Exe</OutputType> + <TargetFramework>net5.0</TargetFramework> + + <IsPackable>false</IsPackable> + <IsShipping>false</IsShipping> + <DisablePubternalApiCheck>true</DisablePubternalApiCheck> + + <UseAppHost>false</UseAppHost> + <RuntimeIdentifier /> + + <!-- Need to build this project in source build --> + <ExcludeFromSourceBuild>false</ExcludeFromSourceBuild> + </PropertyGroup> + + <ItemGroup> + <Compile Include="$(SharedSourceRoot)CommandLineUtils\**\*.cs" /> + </ItemGroup> + +</Project> diff --git a/src/Components/WebAssembly/Sdk/tools/Program.cs b/src/Components/WebAssembly/Sdk/tools/Program.cs new file mode 100644 index 0000000000000000000000000000000000000000..b00093323d09fa7b8205ab9888675d994de224b2 --- /dev/null +++ b/src/Components/WebAssembly/Sdk/tools/Program.cs @@ -0,0 +1,30 @@ +// 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.Threading; +using Microsoft.CodeAnalysis; + +namespace Microsoft.NET.Sdk.BlazorWebAssembly.Tools +{ + internal static class Program + { + public static int Main(string[] args) + { + DebugMode.HandleDebugSwitch(ref args); + + var cancel = new CancellationTokenSource(); + Console.CancelKeyPress += (sender, e) => { cancel.Cancel(); }; + + var application = new Application( + cancel.Token, + Console.Out, + Console.Error); + + application.Commands.Add(new BrotliCompressCommand(application)); + + return application.Execute(args); + } + } +} diff --git a/src/Components/WebAssembly/Sdk/tools/runtimeconfig.template.json b/src/Components/WebAssembly/Sdk/tools/runtimeconfig.template.json new file mode 100644 index 0000000000000000000000000000000000000000..2c73f398906918d3b5d5f39686fcf55fe1372218 --- /dev/null +++ b/src/Components/WebAssembly/Sdk/tools/runtimeconfig.template.json @@ -0,0 +1,3 @@ +{ + "rollForwardOnNoCandidateFx": 2 +} \ No newline at end of file diff --git a/src/Components/test/E2ETest/Tests/RoutingTest.cs b/src/Components/test/E2ETest/Tests/RoutingTest.cs index 2b0c708097d7971ce231d74420b2cbcd6b92c96b..074e57d3720a8dc418ad29a0a6c5688975bd4cf9 100644 --- a/src/Components/test/E2ETest/Tests/RoutingTest.cs +++ b/src/Components/test/E2ETest/Tests/RoutingTest.cs @@ -565,6 +565,19 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests AssertDidNotLog("I'm not happening..."); } + [Fact] + public void OnNavigate_CanRenderUIForExceptions() + { + var app = Browser.MountTestComponent<TestRouterWithOnNavigate>(); + + // Navigating from one page to another should + // cancel the previous OnNavigate Task + SetUrlViaPushState("/Other"); + + var errorUiElem = Browser.Exists(By.Id("blazor-error-ui"), TimeSpan.FromSeconds(10)); + Assert.NotNull(errorUiElem); + } + private long BrowserScrollY { get => (long)((IJavaScriptExecutor)Browser).ExecuteScript("return window.scrollY"); diff --git a/src/Components/test/testassets/BasicTestApp/RouterTest/TestRouterWithOnNavigate.razor b/src/Components/test/testassets/BasicTestApp/RouterTest/TestRouterWithOnNavigate.razor index 4d51ea9e2555d4d857806870b5455003b8d5d53a..5baa82fc005fba993357e48c724ab32e3ea00106 100644 --- a/src/Components/test/testassets/BasicTestApp/RouterTest/TestRouterWithOnNavigate.razor +++ b/src/Components/test/testassets/BasicTestApp/RouterTest/TestRouterWithOnNavigate.razor @@ -20,7 +20,8 @@ private Dictionary<string, Func<NavigationContext, Task>> preNavigateTasks = new Dictionary<string, Func<NavigationContext, Task>>() { { "LongPage1", new Func<NavigationContext, Task>(TestLoadingPageShows) }, - { "LongPage2", new Func<NavigationContext, Task>(TestOnNavCancel) } + { "LongPage2", new Func<NavigationContext, Task>(TestOnNavCancel) }, + { "Other", new Func<NavigationContext, Task>(TestOnNavException) } }; private async Task OnNavigateAsync(NavigationContext args) @@ -43,4 +44,10 @@ await Task.Delay(2000, args.CancellationToken); Console.WriteLine("I'm not happening..."); } + + public static async Task TestOnNavException(NavigationContext args) + { + await Task.CompletedTask; + throw new Exception("This is an uncaught exception."); + } } diff --git a/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/Microsoft.NET.Sdk.Razor.IntegrationTests.csproj b/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/Microsoft.NET.Sdk.Razor.IntegrationTests.csproj index a1e160322eb73603cca5b227a855bd1e4bcbedb6..2832be128d11c3f6748079ce85cfe6053e0a8c84 100644 --- a/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/Microsoft.NET.Sdk.Razor.IntegrationTests.csproj +++ b/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/Microsoft.NET.Sdk.Razor.IntegrationTests.csproj @@ -1,4 +1,4 @@ -<Project Sdk="Microsoft.NET.Sdk"> +<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <!-- @@ -13,8 +13,11 @@ <!-- Tests do not work on Helix yet --> <BuildHelixPayload>false</BuildHelixPayload> + <TestAppsRoot>$(MSBuildProjectDirectory)\..\..\test\testassets\</TestAppsRoot> </PropertyGroup> + <Import Project="$(SharedSourceRoot)MSBuild.Testing\MSBuild.Testing.targets" /> + <ItemGroup> <None Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" /> </ItemGroup> @@ -24,49 +27,6 @@ <Reference Include="Microsoft.Extensions.DependencyModel" /> </ItemGroup> - <ItemGroup> - <AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute"> - <_Parameter1>Testing.AdditionalRestoreSources</_Parameter1> - <_Parameter2>$(MSBuildThisFileDirectory)..\testassets\PregeneratedPackages</_Parameter2> - </AssemblyAttribute> - - <AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute"> - <_Parameter1>ArtifactsLogDir</_Parameter1> - <_Parameter2>$([MSBuild]::NormalizeDirectory('$(ArtifactsDir)', 'log', '$(_BuildConfig)'))</_Parameter2> - </AssemblyAttribute> - - <AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute"> - <_Parameter1>ProcDumpToolPath</_Parameter1> - <_Parameter2>$(ProcDumpPath)</_Parameter2> - </AssemblyAttribute> - - <AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute"> - <_Parameter1>Testing.RepoRoot</_Parameter1> - <_Parameter2>$(RepoRoot)</_Parameter2> - </AssemblyAttribute> - - <AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute"> - <_Parameter1>MicrosoftNETCoreAppRuntimeVersion</_Parameter1> - <_Parameter2>$(MicrosoftNETCoreAppRuntimeVersion)</_Parameter2> - </AssemblyAttribute> - - <AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute"> - <_Parameter1>DefaultNetCoreTargetFramework</_Parameter1> - <_Parameter2>$(DefaultNetCoreTargetFramework)</_Parameter2> - </AssemblyAttribute> - - <AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute"> - <_Parameter1>MicrosoftNetCompilersToolsetPackageVersion</_Parameter1> - <_Parameter2>$(MicrosoftNetCompilersToolsetPackageVersion)</_Parameter2> - </AssemblyAttribute> - - <AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute"> - <_Parameter1>RazorSdkDirectoryRoot</_Parameter1> - <_Parameter2>$(ArtifactsBinDir)Microsoft.NET.Sdk.Razor\$(Configuration)\sdk-output\</_Parameter2> - </AssemblyAttribute> - - </ItemGroup> - <ItemGroup> <Reference Include="rzc" /> <ProjectReference Include="..\..\test\Microsoft.AspNetCore.Razor.Test.MvcShim.ClassLib\Microsoft.AspNetCore.Razor.Test.MvcShim.ClassLib.csproj" /> diff --git a/src/Shared/E2ETesting/E2ETesting.targets b/src/Shared/E2ETesting/E2ETesting.targets index d0fa6cb856ec4934762587337bd8997bf0715db5..76ced2cce99343ce40dccbcea959f47142f3428c 100644 --- a/src/Shared/E2ETesting/E2ETesting.targets +++ b/src/Shared/E2ETesting/E2ETesting.targets @@ -66,14 +66,9 @@ <_DefaultProjectRoot>$([System.IO.Path]::GetFullPath($(_DefaultProjectFilter)))</_DefaultProjectRoot> </PropertyGroup> <ItemGroup> - <_ContentRootProjectReferencesUnfiltered - Include="@(ReferencePath)" - Condition="'%(ReferencePath.ReferenceSourceTarget)' == 'ProjectReference'" /> - <_ContentRootProjectReferencesFilter - Include="@(_ContentRootProjectReferencesUnfiltered->StartsWith('$(_DefaultProjectRoot)'))" /> <_ContentRootProjectReferences - Include="@(_ContentRootProjectReferencesFilter)" - Condition="'%(Identity)' == 'True'" /> + Include="@(ReferencePath)" + Condition="'%(ReferencePath.ReferenceSourceTarget)' == 'ProjectReference' AND $([System.String]::Copy(%(ReferencePath.MSBuildSourceProjectFile)).StartsWith('$(_DefaultProjectRoot)'))" /> </ItemGroup> </Target> diff --git a/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/Assert.cs b/src/Shared/MSBuild.Testing/Assert.cs similarity index 100% rename from src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/Assert.cs rename to src/Shared/MSBuild.Testing/Assert.cs diff --git a/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/BuildVariables.cs b/src/Shared/MSBuild.Testing/BuildVariables.cs similarity index 83% rename from src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/BuildVariables.cs rename to src/Shared/MSBuild.Testing/BuildVariables.cs index 797d244625016737352c4a09c028487ff579b9a2..b659ba918c7afeada9ed60751afe58e65d2fe90a 100644 --- a/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/BuildVariables.cs +++ b/src/Shared/MSBuild.Testing/BuildVariables.cs @@ -19,8 +19,12 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests public static string RazorSdkDirectoryRoot => TestAssemblyMetadata.SingleOrDefault(a => a.Key == "RazorSdkDirectoryRoot").Value; + public static string BlazorWebAssemblySdkDirectoryRoot => TestAssemblyMetadata.SingleOrDefault(a => a.Key == "BlazorWebAssemblySdkDirectoryRoot").Value; + public static string RepoRoot => TestAssemblyMetadata.SingleOrDefault(a => a.Key == "Testing.RepoRoot").Value; - + public static string DefaultNetCoreTargetFramework => TestAssemblyMetadata.SingleOrDefault(a => a.Key == "DefaultNetCoreTargetFramework").Value; + + public static string TestAppsRoot => TestAssemblyMetadata.SingleOrDefault(a => a.Key == "TestAppsRoot").Value; } } diff --git a/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/FIleThumbPrint.cs b/src/Shared/MSBuild.Testing/FIleThumbPrint.cs similarity index 100% rename from src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/FIleThumbPrint.cs rename to src/Shared/MSBuild.Testing/FIleThumbPrint.cs diff --git a/src/Shared/MSBuild.Testing/MSBuild.Testing.targets b/src/Shared/MSBuild.Testing/MSBuild.Testing.targets new file mode 100644 index 0000000000000000000000000000000000000000..8868a23d9791d63136476e6007809cf3a6e14ec5 --- /dev/null +++ b/src/Shared/MSBuild.Testing/MSBuild.Testing.targets @@ -0,0 +1,53 @@ +<Project> + +<ItemGroup> + <AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute"> + <_Parameter1>ArtifactsLogDir</_Parameter1> + <_Parameter2>$([MSBuild]::NormalizeDirectory('$(ArtifactsDir)', 'log', '$(_BuildConfig)'))</_Parameter2> + </AssemblyAttribute> + + <AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute"> + <_Parameter1>ProcDumpToolPath</_Parameter1> + <_Parameter2>$(ProcDumpPath)</_Parameter2> + </AssemblyAttribute> + + <AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute"> + <_Parameter1>Testing.RepoRoot</_Parameter1> + <_Parameter2>$(RepoRoot)</_Parameter2> + </AssemblyAttribute> + + <AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute"> + <_Parameter1>MicrosoftNETCoreAppRuntimeVersion</_Parameter1> + <_Parameter2>$(MicrosoftNETCoreAppRuntimeVersion)</_Parameter2> + </AssemblyAttribute> + + <AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute"> + <_Parameter1>DefaultNetCoreTargetFramework</_Parameter1> + <_Parameter2>$(DefaultNetCoreTargetFramework)</_Parameter2> + </AssemblyAttribute> + + <AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute"> + <_Parameter1>MicrosoftNetCompilersToolsetPackageVersion</_Parameter1> + <_Parameter2>$(MicrosoftNetCompilersToolsetPackageVersion)</_Parameter2> + </AssemblyAttribute> + + <AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute"> + <_Parameter1>RazorSdkDirectoryRoot</_Parameter1> + <_Parameter2>$(ArtifactsBinDir)Microsoft.NET.Sdk.Razor\$(Configuration)\sdk-output\</_Parameter2> + </AssemblyAttribute> + + <AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute"> + <_Parameter1>BlazorWebAssemblySdkDirectoryRoot</_Parameter1> + <_Parameter2>$(ArtifactsBinDir)Microsoft.NET.Sdk.BlazorWebAssembly\$(Configuration)\sdk-output\</_Parameter2> + </AssemblyAttribute> + + <AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute"> + <_Parameter1>TestAppsRoot</_Parameter1> + <_Parameter2>$(TestAppsRoot)</_Parameter2> + </AssemblyAttribute> + </ItemGroup> + + <ItemGroup> + <Compile Include="$(MSBuildThisFileDirectory)*.cs" LinkBase="Infrastructure" /> + </ItemGroup> +</Project> \ No newline at end of file diff --git a/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/MSBuildProcessKind.cs b/src/Shared/MSBuild.Testing/MSBuildProcessKind.cs similarity index 100% rename from src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/MSBuildProcessKind.cs rename to src/Shared/MSBuild.Testing/MSBuildProcessKind.cs diff --git a/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/MSBuildProcessManager.cs b/src/Shared/MSBuild.Testing/MSBuildProcessManager.cs similarity index 98% rename from src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/MSBuildProcessManager.cs rename to src/Shared/MSBuild.Testing/MSBuildProcessManager.cs index cc79523d5bf16ce44c125e247f37a2e5cfa53349..3d9ada158bf8d1d74f17392bd469610de31fa74a 100644 --- a/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/MSBuildProcessManager.cs +++ b/src/Shared/MSBuild.Testing/MSBuildProcessManager.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// 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; @@ -37,6 +37,7 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests $"/p:MicrosoftNETCoreAppRuntimeVersion={BuildVariables.MicrosoftNETCoreAppRuntimeVersion}", $"/p:MicrosoftNetCompilersToolsetPackageVersion={BuildVariables.MicrosoftNetCompilersToolsetPackageVersion}", $"/p:RazorSdkDirectoryRoot={BuildVariables.RazorSdkDirectoryRoot}", + $"/p:BlazorWebAssemblySdkDirectoryRoot={BuildVariables.BlazorWebAssemblySdkDirectoryRoot}", $"/p:RepoRoot={BuildVariables.RepoRoot}", $"/p:Configuration={project.Configuration}", $"/t:{target}", diff --git a/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/MSBuildResult.cs b/src/Shared/MSBuild.Testing/MSBuildResult.cs similarity index 100% rename from src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/MSBuildResult.cs rename to src/Shared/MSBuild.Testing/MSBuildResult.cs diff --git a/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/ProjectDirectory.cs b/src/Shared/MSBuild.Testing/ProjectDirectory.cs similarity index 97% rename from src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/ProjectDirectory.cs rename to src/Shared/MSBuild.Testing/ProjectDirectory.cs index d0d0e571f46ba988504ce7a654e69324356ced70..0f669c31fceeb36e5cc7ed55ffbf1be38d6f931f 100644 --- a/src/Razor/Microsoft.NET.Sdk.Razor/integrationtests/ProjectDirectory.cs +++ b/src/Shared/MSBuild.Testing/ProjectDirectory.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// 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; @@ -49,12 +49,11 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests } var repositoryRoot = BuildVariables.RepoRoot; - var solutionRoot = Path.Combine(repositoryRoot, "src", "Razor"); var binariesRoot = Path.GetDirectoryName(typeof(ProjectDirectory).Assembly.Location); + var testAppsRoot = BuildVariables.TestAppsRoot; foreach (var project in new string[] { originalProjectName, }.Concat(additionalProjects)) { - var testAppsRoot = Path.Combine(solutionRoot, "test", "testassets"); var projectRoot = Path.Combine(testAppsRoot, project); if (!Directory.Exists(projectRoot)) { @@ -146,6 +145,11 @@ $@"<Project> .ForEach(file => { var source = Path.Combine(testAppsRoot, file); + if (!File.Exists(source)) + { + return; + } + var destination = Path.Combine(projectDestination, file); File.Copy(source, destination); }); diff --git a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/DefaultHttpClient.java b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/DefaultHttpClient.java index 58a95445c87f3f470f46cc6bb4b9a297173ba9d8..8f4f4f0df91d1c9a8c8acf701a18739bc45a6e94 100644 --- a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/DefaultHttpClient.java +++ b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/DefaultHttpClient.java @@ -19,14 +19,14 @@ import okhttp3.*; final class DefaultHttpClient extends HttpClient { private OkHttpClient client = null; - public DefaultHttpClient() { - this(0, null); + public DefaultHttpClient(Action1<OkHttpClient.Builder> configureBuilder) { + this(null, configureBuilder); } public DefaultHttpClient cloneWithTimeOut(int timeoutInMilliseconds) { OkHttpClient newClient = client.newBuilder().readTimeout(timeoutInMilliseconds, TimeUnit.MILLISECONDS) .build(); - return new DefaultHttpClient(timeoutInMilliseconds, newClient); + return new DefaultHttpClient(newClient, null); } @Override @@ -36,7 +36,7 @@ final class DefaultHttpClient extends HttpClient { } } - public DefaultHttpClient(int timeoutInMilliseconds, OkHttpClient client) { + public DefaultHttpClient(OkHttpClient client, Action1<OkHttpClient.Builder> configureBuilder) { if (client != null) { this.client = client; } else { @@ -90,9 +90,10 @@ final class DefaultHttpClient extends HttpClient { } }); - if (timeoutInMilliseconds > 0) { - builder.readTimeout(timeoutInMilliseconds, TimeUnit.MILLISECONDS); + if (configureBuilder != null) { + configureBuilder.invoke(builder); } + this.client = builder.build(); } } diff --git a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/HttpHubConnectionBuilder.java b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/HttpHubConnectionBuilder.java index d91e382ed915d284e73a2a2691bd6b80f9b0c14b..e1f38e6888a43a4c950a10e1448ac6d6b657e2d9 100644 --- a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/HttpHubConnectionBuilder.java +++ b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/HttpHubConnectionBuilder.java @@ -7,6 +7,7 @@ import java.util.HashMap; import java.util.Map; import io.reactivex.Single; +import okhttp3.OkHttpClient; /** * A builder for configuring {@link HubConnection} instances. @@ -20,6 +21,7 @@ public class HttpHubConnectionBuilder { private long handshakeResponseTimeout = 0; private Map<String, String> headers; private TransportEnum transportEnum; + private Action1<OkHttpClient.Builder> configureBuilder; HttpHubConnectionBuilder(String url) { this.url = url; @@ -113,12 +115,25 @@ public class HttpHubConnectionBuilder { return this; } + /** + * Sets a method that will be called when constructing the HttpClient to allow customization such as certificate validation, proxies, and cookies. + * By default the client will have a cookie jar added and a read timeout for LongPolling. + * + * @param configureBuilder Callback for configuring the OkHttpClient.Builder. + * @return This instance of the HttpHubConnectionBuilder. + */ + public HttpHubConnectionBuilder setHttpClientBuilderCallback(Action1<OkHttpClient.Builder> configureBuilder) { + this.configureBuilder = configureBuilder; + return this; + } + /** * Builds a new instance of {@link HubConnection}. * * @return A new instance of {@link HubConnection}. */ public HubConnection build() { - return new HubConnection(url, transport, skipNegotiate, httpClient, accessTokenProvider, handshakeResponseTimeout, headers, transportEnum); + return new HubConnection(url, transport, skipNegotiate, httpClient, accessTokenProvider, + handshakeResponseTimeout, headers, transportEnum, configureBuilder); } } diff --git a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/HubConnection.java b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/HubConnection.java index 5c884cfff142397667c606406dc70c7fcc57c6bf..addbd5c2f6aea2e6173862028dc3f032b7e378af 100644 --- a/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/HubConnection.java +++ b/src/SignalR/clients/java/signalr/src/main/java/com/microsoft/signalr/HubConnection.java @@ -20,6 +20,7 @@ import io.reactivex.Completable; import io.reactivex.Observable; import io.reactivex.Single; import io.reactivex.subjects.*; +import okhttp3.OkHttpClient; /** * A connection used to invoke hub methods on a SignalR Server. @@ -126,7 +127,8 @@ public class HubConnection implements AutoCloseable { } HubConnection(String url, Transport transport, boolean skipNegotiate, HttpClient httpClient, - Single<String> accessTokenProvider, long handshakeResponseTimeout, Map<String, String> headers, TransportEnum transportEnum) { + Single<String> accessTokenProvider, long handshakeResponseTimeout, Map<String, String> headers, TransportEnum transportEnum, + Action1<OkHttpClient.Builder> configureBuilder) { if (url == null || url.isEmpty()) { throw new IllegalArgumentException("A valid url is required."); } @@ -143,7 +145,7 @@ public class HubConnection implements AutoCloseable { if (httpClient != null) { this.httpClient = httpClient; } else { - this.httpClient = new DefaultHttpClient(); + this.httpClient = new DefaultHttpClient(configureBuilder); } if (transport != null) { diff --git a/src/SignalR/clients/java/signalr/src/test/java/com/microsoft/signalr/WebSocketTransportTest.java b/src/SignalR/clients/java/signalr/src/test/java/com/microsoft/signalr/WebSocketTransportTest.java index 5edda35267c5c1e79d81f9e56818dc0689477d1f..4aeec16836ee34ca11acfd801d3544043b6edd84 100644 --- a/src/SignalR/clients/java/signalr/src/test/java/com/microsoft/signalr/WebSocketTransportTest.java +++ b/src/SignalR/clients/java/signalr/src/test/java/com/microsoft/signalr/WebSocketTransportTest.java @@ -16,13 +16,6 @@ import io.reactivex.Completable; import io.reactivex.Single; class WebSocketTransportTest { - // @Test Skipping until we add functional test support - public void WebSocketThrowsIfItCantConnect() { - Transport transport = new WebSocketTransport(new HashMap<>(), new DefaultHttpClient()); - RuntimeException exception = assertThrows(RuntimeException.class, () -> transport.start("http://url.fake.example").blockingAwait(1, TimeUnit.SECONDS)); - assertEquals("There was an error starting the WebSocket transport.", exception.getMessage()); - } - @Test public void CanPassNullExitCodeToOnClosed() { WebSocketTransport transport = new WebSocketTransport(new HashMap<>(), new WebSocketTestHttpClient());