From 0ca2ed9af69e7e334b8e3c1de1d015017f138988 Mon Sep 17 00:00:00 2001 From: Javier Calvarro Nelson <jacalvar@microsoft.com> Date: Tue, 17 Aug 2021 10:52:12 +0200 Subject: [PATCH] [Blazor] Custom JS initializers (#34798) * Users can create an ES6 module with `<packageName>.lib.module.js` and declare a beforeStart and afterStarted function to integrate into the blazor start process. * In WebAssembly beforeStart receives the BlazorWebAssemblyStartOptions object and any publish extension as part of the method arguments. * In Server beforeStart receives the CircuitStartOptions object and can configure any necessary details. * In Desktop beforeStart does not receive any argument for the time being. * afterStarted receives the Blazor object as argument in all cases. * Callbacks in beforeStart and afterStarted are not invoked in any defined order and are invoked in parallel. --- .../Authorization/src/AuthorizeRouteView.cs | 5 + .../ComponentEndpointConventionBuilder.cs | 5 +- ...ComponentEndpointRouteBuilderExtensions.cs | 10 +- ...rcuitJavaScriptInitializationMiddleware.cs | 26 ++ src/Components/Server/src/CircuitOptions.cs | 5 +- ...ionsJavaScriptInitializersConfiguration.cs | 33 ++ src/Components/Server/src/ComponentHub.cs | 1 + .../ComponentServiceCollectionExtensions.cs | 1 + ...onentEndpointRouteBuilderExtensionsTest.cs | 7 +- .../Web.JS/dist/Release/blazor.server.js | Bin 128426 -> 129140 bytes .../Web.JS/dist/Release/blazor.webview.js | Bin 39334 -> 40069 bytes src/Components/Web.JS/src/Boot.Server.ts | 6 +- src/Components/Web.JS/src/Boot.WebAssembly.ts | 19 +- src/Components/Web.JS/src/Boot.WebView.ts | 4 + .../JSInitializers/JSInitializers.Server.ts | 16 + .../JSInitializers.WebAssembly.ts | 15 + .../JSInitializers/JSInitializers.WebView.ts | 14 + .../src/JSInitializers/JSInitializers.ts | 40 +++ .../Web.JS/src/Platform/BootConfig.ts | 34 +- .../src/Platform/WebAssemblyStartOptions.ts | 2 +- src/Components/Web.JS/tsconfig.json | 1 + .../src/JSComponents/JSComponentInterop.cs | 3 + .../Web/src/WebEventData/WebEventData.cs | 2 + .../Server/Properties/launchSettings.json | 16 +- .../src/HotReload/HotReloadAgent.cs | 3 + .../PhotinoTestApp/PhotinoTestApp.csproj | 4 +- ...osoft.AspNetCore.Components.WebView.csproj | 8 +- .../WebView/src/StaticWebAssetsLoader.cs | 292 ------------------ .../WebView/WebView/src/WebViewManager.cs | 35 +++ .../WebView/WebView/src/blazor.modules.json | 1 + ...rosoft.AspNetCore.Components.WebView.props | 5 + .../JsInitializersTest.cs | 24 ++ .../test/E2ETest/Tests/JsInitializersTest.cs | 43 +++ .../wwwroot/BasicTestApp.lib.module.js | 48 +++ .../ComponentFromPackage.razor | 15 +- .../ComponentFromPackage.razor.js | 5 + .../src/Microsoft.AspNetCore.Hosting.csproj | 1 + .../StaticWebAssets/StaticWebAssetsLoader.cs | 63 +--- .../StaticWebAssets/StaticWebAssetsReader.cs | 75 ----- ...anifestStaticWebAssetsFileProviderTests.cs | 5 +- .../StaticWebAssetsFileProviderTests.cs | 210 ------------- .../StaticWebAssetsLoaderTests.cs | 116 ------- .../Infrastructure/ServerFactory.cs | 59 ---- .../StaticWebAssetsFileProvider.cs | 149 +-------- 44 files changed, 448 insertions(+), 978 deletions(-) create mode 100644 src/Components/Server/src/CircuitJavaScriptInitializationMiddleware.cs create mode 100644 src/Components/Server/src/Circuits/CircuitOptionsJavaScriptInitializersConfiguration.cs create mode 100644 src/Components/Web.JS/src/JSInitializers/JSInitializers.Server.ts create mode 100644 src/Components/Web.JS/src/JSInitializers/JSInitializers.WebAssembly.ts create mode 100644 src/Components/Web.JS/src/JSInitializers/JSInitializers.WebView.ts create mode 100644 src/Components/Web.JS/src/JSInitializers/JSInitializers.ts delete mode 100644 src/Components/WebView/WebView/src/StaticWebAssetsLoader.cs create mode 100644 src/Components/WebView/WebView/src/blazor.modules.json create mode 100644 src/Components/WebView/WebView/src/buildTransitive/any/Microsoft.AspNetCore.Components.WebView.props create mode 100644 src/Components/test/E2ETest/ServerExecutionTests/JsInitializersTest.cs create mode 100644 src/Components/test/E2ETest/Tests/JsInitializersTest.cs create mode 100644 src/Components/test/testassets/BasicTestApp/wwwroot/BasicTestApp.lib.module.js create mode 100644 src/Components/test/testassets/TestContentPackage/ComponentFromPackage.razor.js delete mode 100644 src/Hosting/Hosting/src/StaticWebAssets/StaticWebAssetsReader.cs delete mode 100644 src/Hosting/Hosting/test/StaticWebAssets/StaticWebAssetsFileProviderTests.cs delete mode 100644 src/Hosting/Hosting/test/StaticWebAssets/StaticWebAssetsLoaderTests.cs rename src/{Hosting/Hosting/src => Shared}/StaticWebAssets/StaticWebAssetsFileProvider.cs (74%) diff --git a/src/Components/Authorization/src/AuthorizeRouteView.cs b/src/Components/Authorization/src/AuthorizeRouteView.cs index ef1fb45dfd9..291da9bf12d 100644 --- a/src/Components/Authorization/src/AuthorizeRouteView.cs +++ b/src/Components/Authorization/src/AuthorizeRouteView.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Components.Rendering; @@ -95,6 +96,10 @@ namespace Microsoft.AspNetCore.Components.Authorization builder.CloseComponent(); } + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2111:RequiresUnreferencedCode", + Justification = "OpenComponent already has the right set of attributes")] + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2110:RequiresUnreferencedCode", + Justification = "OpenComponent already has the right set of attributes")] private void RenderContentInDefaultLayout(RenderTreeBuilder builder, RenderFragment content) { builder.OpenComponent<LayoutView>(0); diff --git a/src/Components/Server/src/Builder/ComponentEndpointConventionBuilder.cs b/src/Components/Server/src/Builder/ComponentEndpointConventionBuilder.cs index 66c8c260e8c..e8f40cb25a7 100644 --- a/src/Components/Server/src/Builder/ComponentEndpointConventionBuilder.cs +++ b/src/Components/Server/src/Builder/ComponentEndpointConventionBuilder.cs @@ -12,11 +12,13 @@ namespace Microsoft.AspNetCore.Builder { private readonly IEndpointConventionBuilder _hubEndpoint; private readonly IEndpointConventionBuilder _disconnectEndpoint; + private readonly IEndpointConventionBuilder _jsInitializersEndpoint; - internal ComponentEndpointConventionBuilder(IEndpointConventionBuilder hubEndpoint, IEndpointConventionBuilder disconnectEndpoint) + internal ComponentEndpointConventionBuilder(IEndpointConventionBuilder hubEndpoint, IEndpointConventionBuilder disconnectEndpoint, IEndpointConventionBuilder jsInitializersEndpoint) { _hubEndpoint = hubEndpoint; _disconnectEndpoint = disconnectEndpoint; + _jsInitializersEndpoint = jsInitializersEndpoint; } /// <summary> @@ -27,6 +29,7 @@ namespace Microsoft.AspNetCore.Builder { _hubEndpoint.Add(convention); _disconnectEndpoint.Add(convention); + _jsInitializersEndpoint.Add(convention); } } } diff --git a/src/Components/Server/src/Builder/ComponentEndpointRouteBuilderExtensions.cs b/src/Components/Server/src/Builder/ComponentEndpointRouteBuilderExtensions.cs index 2b021d4aa08..aebe39281f0 100644 --- a/src/Components/Server/src/Builder/ComponentEndpointRouteBuilderExtensions.cs +++ b/src/Components/Server/src/Builder/ComponentEndpointRouteBuilderExtensions.cs @@ -3,9 +3,12 @@ using System; using Microsoft.AspNetCore.Components.Server; +using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http.Connections; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.SignalR; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; namespace Microsoft.AspNetCore.Builder { @@ -110,7 +113,12 @@ namespace Microsoft.AspNetCore.Builder endpoints.CreateApplicationBuilder().UseMiddleware<CircuitDisconnectMiddleware>().Build()) .WithDisplayName("Blazor disconnect"); - return new ComponentEndpointConventionBuilder(hubEndpoint, disconnectEndpoint); + var jsInitializersEndpoint = endpoints.Map( + (path.EndsWith('/') ? path : path + "/") + "initializers/", + endpoints.CreateApplicationBuilder().UseMiddleware<CircuitJavaScriptInitializationMiddleware>().Build()) + .WithDisplayName("Blazor initializers"); + + return new ComponentEndpointConventionBuilder(hubEndpoint, disconnectEndpoint, jsInitializersEndpoint); } } } diff --git a/src/Components/Server/src/CircuitJavaScriptInitializationMiddleware.cs b/src/Components/Server/src/CircuitJavaScriptInitializationMiddleware.cs new file mode 100644 index 00000000000..845f65308ea --- /dev/null +++ b/src/Components/Server/src/CircuitJavaScriptInitializationMiddleware.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Components.Server; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Builder +{ + internal class CircuitJavaScriptInitializationMiddleware + { + private readonly IList<string> _initializers; + + // We don't need the request delegate for anything, however we need to inject it to satisfy the middleware + // contract. + public CircuitJavaScriptInitializationMiddleware(IOptions<CircuitOptions> options, RequestDelegate _) + { + _initializers = options.Value.JavaScriptInitializers; + } + + public async Task InvokeAsync(HttpContext context) + { + await context.Response.WriteAsJsonAsync(_initializers); + } + } +} diff --git a/src/Components/Server/src/CircuitOptions.cs b/src/Components/Server/src/CircuitOptions.cs index 1d6323e53f3..2eb51b174f4 100644 --- a/src/Components/Server/src/CircuitOptions.cs +++ b/src/Components/Server/src/CircuitOptions.cs @@ -1,8 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; -using Microsoft.AspNetCore.Components.Web; +using Microsoft.Extensions.Hosting; namespace Microsoft.AspNetCore.Components.Server { @@ -82,5 +81,7 @@ namespace Microsoft.AspNetCore.Components.Server /// Gets options for root components within the circuit. /// </summary> public CircuitRootComponentOptions RootComponents { get; } = new CircuitRootComponentOptions(); + + internal IList<string> JavaScriptInitializers { get; } = new List<string>(); } } diff --git a/src/Components/Server/src/Circuits/CircuitOptionsJavaScriptInitializersConfiguration.cs b/src/Components/Server/src/Circuits/CircuitOptionsJavaScriptInitializersConfiguration.cs new file mode 100644 index 00000000000..e1e70427b9a --- /dev/null +++ b/src/Components/Server/src/Circuits/CircuitOptionsJavaScriptInitializersConfiguration.cs @@ -0,0 +1,33 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Components.Server.Circuits +{ + internal class CircuitOptionsJavaScriptInitializersConfiguration : IConfigureOptions<CircuitOptions> + { + private readonly IWebHostEnvironment _environment; + + public CircuitOptionsJavaScriptInitializersConfiguration(IWebHostEnvironment environment) + { + _environment = environment; + } + + public void Configure(CircuitOptions options) + { + var file = _environment.WebRootFileProvider.GetFileInfo($"{_environment.ApplicationName}.modules.json"); + if (file.Exists) + { + var initializers = JsonSerializer.Deserialize<string[]>(file.CreateReadStream()); + for (var i = 0; i < initializers.Length; i++) + { + var initializer = initializers[i]; + options.JavaScriptInitializers.Add(initializer); + } + } + } + } +} diff --git a/src/Components/Server/src/ComponentHub.cs b/src/Components/Server/src/ComponentHub.cs index bb802199a7a..256b3f2e787 100644 --- a/src/Components/Server/src/ComponentHub.cs +++ b/src/Components/Server/src/ComponentHub.cs @@ -13,6 +13,7 @@ using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Components.Server { diff --git a/src/Components/Server/src/DependencyInjection/ComponentServiceCollectionExtensions.cs b/src/Components/Server/src/DependencyInjection/ComponentServiceCollectionExtensions.cs index 3a7cd4ce842..43bd3c85c99 100644 --- a/src/Components/Server/src/DependencyInjection/ComponentServiceCollectionExtensions.cs +++ b/src/Components/Server/src/DependencyInjection/ComponentServiceCollectionExtensions.cs @@ -83,6 +83,7 @@ namespace Microsoft.Extensions.DependencyInjection services.AddScoped<AuthenticationStateProvider, ServerAuthenticationStateProvider>(); services.TryAddEnumerable(ServiceDescriptor.Singleton<IConfigureOptions<CircuitOptions>, CircuitOptionsJSInteropDetailedErrorsConfiguration>()); + services.TryAddEnumerable(ServiceDescriptor.Singleton<IConfigureOptions<CircuitOptions>, CircuitOptionsJavaScriptInitializersConfiguration>()); if (configure != null) { diff --git a/src/Components/Server/test/ComponentEndpointRouteBuilderExtensionsTest.cs b/src/Components/Server/test/ComponentEndpointRouteBuilderExtensionsTest.cs index 7a6e892fc50..977228551a2 100644 --- a/src/Components/Server/test/ComponentEndpointRouteBuilderExtensionsTest.cs +++ b/src/Components/Server/test/ComponentEndpointRouteBuilderExtensionsTest.cs @@ -2,10 +2,11 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; -using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Hosting; using Moq; using Xunit; @@ -54,6 +55,9 @@ namespace Microsoft.AspNetCore.Components.Server.Tests private IApplicationBuilder CreateAppBuilder() { + var environment = new Mock<IWebHostEnvironment>(); + environment.SetupGet(e => e.ApplicationName).Returns("app"); + environment.SetupGet(e => e.WebRootFileProvider).Returns(new NullFileProvider()); var services = new ServiceCollection(); services.AddSingleton(Mock.Of<IHostApplicationLifetime>()); services.AddLogging(); @@ -64,6 +68,7 @@ namespace Microsoft.AspNetCore.Components.Server.Tests services.AddRouting(); services.AddSignalR(); services.AddServerSideBlazor(); + services.AddSingleton(environment.Object); services.AddSingleton<IConfiguration>(new ConfigurationBuilder().Build()); var serviceProvider = services.BuildServiceProvider(); diff --git a/src/Components/Web.JS/dist/Release/blazor.server.js b/src/Components/Web.JS/dist/Release/blazor.server.js index c09fced56bd940af437ddc21d82efdbfb09fdc67..c591e6ab9a26d8f37d612d638d25e070b9171502 100644 GIT binary patch delta 1376 zcmaJ>U1%It6y_u=*=j3-q4-ywjLULwyiO8D)ZNb3Y-=^C5iz!=wyBw&z1zKZ_D*u= zZi3m2`yfaosSobMfC#Np6r{C<JZvdahz~*$AF80~lT_NcX`$r7`rr?GcV^e-p>-bS z+_~qTbH4MP@6H$3x_(>hI(^lIMD3c{1KGt*nGf!U#Nx*<{jyPgRrmHMl<Tzre(jY1 z@Z#7zvAEhDfA23)XWlqmh$-jN!l#`HifTVEY^l9_WJLKd9Fg}b|ISZ}oywd#J_?Yn zUAnZncI-s_!_OaF-1=GWG37XV;l6{AsGoTT?&yGQa4rQO-Jg|aq0DW`T#hJ_xFV=5 z3y!k&sWj|WdFtP8g)W7u{=)=JBV~u@;r!Doyn6}GDD&MCjBZSfo04Ra#F%93<ZC$J ztq%VFBWzNzgV-`?@oX!7aPlX3D0p!hJeB&ZW%&NCs_j^wr_J(;?Q&0wvMpVKa77lG zXIKSE#i+D|r1|ZZ<K!&+kZ0~2_bu;jZfmSG=?Xc-nPis3W~uN7oBL5G){MoZ_KI*z z%%g@1M${--lL%2W6NQJJ$D`Ic4*t9dTWZfa8>%_Wqq|=nN=IC1(x5!=z0PD2>3urh zHj$`EKT)CnL@HeNg=-&iOk6Xn8UaHBr&$3_yDYEuo2KdFC}TS8w34F*SJ1G#lt*s1 zTBZ3E^G&y1pCywD|E-uXCd*zCF-*K|9x@C=;#5@3M@{+&#p-Eu#i!jvbnsSb5XF^s zZ0&E+b^8fKJW4YoRcNz>?@*}?bF*iFOf}!>bp$JZwLNP5xT3h23hI|(0H%V~%W%AQ zZf0}s?8|pnqv8cBZAI~$ImeoHMIT!?R-IHzR2JQQN`K+`*K}eFny0)ubuXneZad{X zRlAm5q$!=dkB2MW$Aq^AB8|5^C2>rI$?1$zTbtZw>0YS`<91r&WFH+@af(v)4i>J! zvmk<RuE2;g-77EzZg5})devQ+UxBmxJu|^k-nGjm1rQS!*4x36OT4)`UB|>~LE6QU zB!aL|BRwtM7+zEC+nKP-Od<=n6r+V>6~WP8;fVpeNpL73%`p)+GfiT9Y#d!6jfNF> zx}vnhk`|FB3pyI`DA+-=0U2;=2M#_ER2vWnCpg@Ifx%(ks`6i2Zqm8A2+>WHt=rHB zDyV|MsVxx?{%Am-VxxBzzEtMoDrBA{++hEsxZSj^%l7X_+f_$b;h)afFi$3fzZ;+j uBiG@nuBNK`$yj~<Iutr!tiHAekHlasxaS7+!=7O9225;F^1?nwIQ=gN3h_Ds delta 720 zcmZ8eT}TvB6z1sKuBky}nUytbgWOv~>_(Ba&IXh8vzt$XWMnvvvo>INa^|kmx?6-0 z6&leHmqO@=D3C;9A4Zl4>`kJ;f|6ds9&Fw9AuFnfpzgBTgD>~H_k8Dk=ey_p`05?a zdna--C{AAXgP5)g-&_YudgR3CO8&as9uPS-k-FiGr?#h0Uh?>OwBza@;5nzR#XOvx z8hTW!=*amrRObv1w{U)Dm~P<wk%#9>IgCGU1rVLNxlPW4XTIA{x2Bt(oZZcd`!k!n zL2)M=p{xW%_TnJis}-pnixZ6+CPBJH@qne%xJ8iY_J*LH>$tC)z{@?k?>eCmag1Dt z=>xpHdI}~vyq<#AN~J@ll87kAlITh=VY-?p{(KKr+&1&fg5VRE<=NN=*v5{{g3YC$ z&%)aZ+@n(=L1ceWiWJU}YF{Lpu;NPS<&}8Ah@l9(GY`SCLQ=pPIbO%I^DxNvXQ2@Y zBUxzSaAgrTvY9OG1B<O$fL+{RZ~-!>O<5tR%hclMbwaUZ%S7)NnhCNbGnuTf$C4qd z{i;EQHiD=`BpZi#M}IM9@3E<B(ZhzN#SMxKjEh4O`b?usAna%k_SUEc>zYYyDrhpo za=2jGud-V?s0Evi=3pnN>`e~BpgCu{x0J_8kEwRmOBxI0A=sQC%c}onELl$`SG!v7 zykgAKc}t>e6j}HXVi&Uf`<Xn{tc>DI9_|+J;q|FigKcxaEyCYYPl7Zw6f4&*f#NN! mA|OTG)+LCQK-A5AhaDb>vX8$Y0KKf@H*}Wso!}HkIP?$s;sO@{ diff --git a/src/Components/Web.JS/dist/Release/blazor.webview.js b/src/Components/Web.JS/dist/Release/blazor.webview.js index 0b09560ad7a2b07bb43f60a3fa2265f6f8f4293c..ab6e66d75bc964ee375effa58458cef2c5c26286 100644 GIT binary patch delta 1150 zcmaJ>O-vI(6izWQsa_xkDHxM=MVKYa0unE6wg_SjhbBZt2`wqZc4`;4GtJBt0?nqS zQhtI_k~ye}2QGTXR1d}@7vsT;i3h!V@#59!!P(t5NCNKR<=gkYpLyS#-B<ON+Pjwh zH}TMdcpD#TPXMeVoohOFvqfxin_wkT{~bSbA(RlOXQo4mdV1z{NJh7%)}%|MMkM~6 z+LT0cc1#st%jd*?xldBx%J-zbT6rq%$I34mwpZ;HKh`cb&|p<<pw;RXA0?&zQ!PlF zfA~_`-CBp}TYuC*pVm7i+S}+7Ewza6i-_Tx(m+n_vX5T)wnL0>bV=kzuWa=AXxe8Y zqHWV-c54wazB%eMW8%YRP<u>Nw!)%+tF_^;Y#qk-`P}!disT+_n|_V+_4nJvQ^Pyz z!A_@0?o0-6^@E+Wr^=QC8B-`PSp+jqixwvoXeFMrnE|sL(K{Scjxsl(<IF>Ak(tR^ z7qS&>DR#a<D4)PKx1nRNBFaWRKk&9wg3Hk6$|NOun;}Cc17zf30RUvigLHp;t#W-b z0c$~jg^JeQc5&Sam**ivQ@1B#6!9X(3O5kWu=_U80d+vtM&`Op-0dzQcP{F~0Ux^n zYdSH5=E3;@)naxQED<}S44S4%w4h?lAGkD+vV<ak$SBkgCoZE*l({Bp_Gdjka*X2y zF$zVN15DH52n2><a4i}%bAhL#jx_l+CBsW(5se;K2B0INO*K?Ic@cq}+>CRX>a}nk z#1+ELOGB}L6!yLk$mL3<2t@pRJXH2V27~bx-q7l3f{ganQBZ@<B5q0H(^(4hXqnK( zfq4h65^Cg0rsyDMEHHvqy_82hM>0|M*3G-BZc&s$*t;Aos@m9ciWwxmX=vq8RK=v< z@{sCkdh-;>uLI<_ZDO=6Z)H>cV4>JJ>B@0EIqUrqfEEMw-Y3_>Ax)&7eHqq+L~hvs Z5k8GHH;oMDU33!)^Z8uZ6U^0QwZBDStDFD; delta 416 zcmZqO$+T=U(}pc}jJ}h%+8K&wY2@jo>Xc~e<khB?<|UV8=I1G7ZT@Y?CC!*MnJp@U zF>7;t)J8_2YQdOBpwQ$Pdm#IAOgm5_I@U>P@~wK|$s6jmfKs>X!+@kw!*n2ds^KS4 z*@7nZ$*&t_p)7+YB`B+^NfpeB29kf83V>v8^HLx=p;dgcR!a|*b*@Dc$Xd`QHJPbZ z0xTvm*|=2!%F1a~2D9dXN%6@pZBjrMNOfhK8kiLUmXVk&*bb7J&?+(6rQH!M<23n1 zJH%|K$qgNxlXW`;pyCZ32(?;Zxn~^>K)LizTd)~on|F8SgF*x3)WhA9lcT!>AiT|o zySbTJb5ctbiY9NIAi|knqM=!xnWj-R`P>9UwxSYSMFXA9cPE;1P8Obhjk`8JRj;@t T5h$ELS!PZGSaq$YhNd+Dc-Egn diff --git a/src/Components/Web.JS/src/Boot.Server.ts b/src/Components/Web.JS/src/Boot.Server.ts index 5e23268a67d..9f5fc9e608b 100644 --- a/src/Components/Web.JS/src/Boot.Server.ts +++ b/src/Components/Web.JS/src/Boot.Server.ts @@ -13,6 +13,7 @@ import { DefaultReconnectionHandler } from './Platform/Circuits/DefaultReconnect import { attachRootComponentToLogicalElement } from './Rendering/Renderer'; import { discoverComponents, discoverPersistedState, ServerComponentDescriptor } from './Services/ComponentDescriptorDiscovery'; import { sendJSDataStream } from './Platform/Circuits/CircuitStreamingInterop'; +import { fetchAndInvokeInitializers } from './JSInitializers/JSInitializers.Server'; let renderingFailed = false; let started = false; @@ -25,6 +26,8 @@ async function boot(userOptions?: Partial<CircuitStartOptions>): Promise<void> { // Establish options to be used const options = resolveOptions(userOptions); + const jsInitializer = await fetchAndInvokeInitializers(options); + const logger = new ConsoleLogger(options.logLevel); Blazor.defaultReconnectionHandler = new DefaultReconnectionHandler(logger); @@ -35,7 +38,6 @@ async function boot(userOptions?: Partial<CircuitStartOptions>): Promise<void> { const appState = discoverPersistedState(document); const circuit = new CircuitDescriptor(components, appState || ''); - const initialConnection = await initializeConnection(options, logger, circuit); const circuitStarted = await circuit.startCircuit(initialConnection); if (!circuitStarted) { @@ -77,6 +79,8 @@ async function boot(userOptions?: Partial<CircuitStartOptions>): Promise<void> { Blazor.reconnect = reconnect; logger.log(LogLevel.Information, 'Blazor server-side application started.'); + + jsInitializer.invokeAfterStartedCallbacks(Blazor); } async function initializeConnection(options: CircuitStartOptions, logger: Logger, circuit: CircuitDescriptor): Promise<HubConnection> { diff --git a/src/Components/Web.JS/src/Boot.WebAssembly.ts b/src/Components/Web.JS/src/Boot.WebAssembly.ts index 13b6f2b4432..76e103a1f12 100644 --- a/src/Components/Web.JS/src/Boot.WebAssembly.ts +++ b/src/Components/Web.JS/src/Boot.WebAssembly.ts @@ -14,6 +14,8 @@ import { WebAssemblyStartOptions } from './Platform/WebAssemblyStartOptions'; import { WebAssemblyComponentAttacher } from './Platform/WebAssemblyComponentAttacher'; import { discoverComponents, discoverPersistedState, WebAssemblyComponentDescriptor } from './Services/ComponentDescriptorDiscovery'; import { setDispatchEventMiddleware } from './Rendering/WebRendererInteropMethods'; +import { AfterBlazorStartedCallback, JSInitializer } from './JSInitializers/JSInitializers'; +import { fetchAndInvokeInitializers } from './JSInitializers/JSInitializers.WebAssembly'; declare var Module: EmscriptenModule; let started = false; @@ -80,11 +82,13 @@ async function boot(options?: Partial<WebAssemblyStartOptions>): Promise<void> { ); }); - // Get the custom environment setting if defined - const environment = options?.environment; + const candidateOptions = options ?? {}; + + // Get the custom environment setting and blazorBootJson loader if defined + const environment = candidateOptions.environment; // Fetch the resources and prepare the Mono runtime - const bootConfigPromise = BootConfigResult.initAsync(environment); + const bootConfigPromise = BootConfigResult.initAsync(candidateOptions.loadBootResource, environment); // Leverage the time while we are loading boot.config.json from the network to discover any potentially registered component on // the document. @@ -110,9 +114,11 @@ async function boot(options?: Partial<WebAssemblyStartOptions>): Promise<void> { } }; - const bootConfigResult = await bootConfigPromise; + const bootConfigResult: BootConfigResult = await bootConfigPromise; + const jsInitializer = await fetchAndInvokeInitializers(bootConfigResult.bootConfig, candidateOptions); + const [resourceLoader] = await Promise.all([ - WebAssemblyResourceLoader.initAsync(bootConfigResult.bootConfig, options || {}), + WebAssemblyResourceLoader.initAsync(bootConfigResult.bootConfig, candidateOptions || {}), WebAssemblyConfigLoader.initAsync(bootConfigResult)]); try { @@ -123,6 +129,9 @@ async function boot(options?: Partial<WebAssemblyStartOptions>): Promise<void> { // Start up the application platform.callEntryPoint(resourceLoader.bootConfig.entryAssembly); + // At this point .NET has been initialized (and has yielded), we can't await the promise becasue it will + // only end when the app finishes running + jsInitializer.invokeAfterStartedCallbacks(Blazor); } function invokeJSFromDotNet(callInfo: Pointer, arg0: any, arg1: any, arg2: any): any { diff --git a/src/Components/Web.JS/src/Boot.WebView.ts b/src/Components/Web.JS/src/Boot.WebView.ts index 0b5a5f17767..b7740928550 100644 --- a/src/Components/Web.JS/src/Boot.WebView.ts +++ b/src/Components/Web.JS/src/Boot.WebView.ts @@ -4,6 +4,7 @@ import { shouldAutoStart } from './BootCommon'; import { internalFunctions as navigationManagerFunctions } from './Services/NavigationManager'; import { startIpcReceiver } from './Platform/WebView/WebViewIpcReceiver'; import { sendAttachPage, sendBeginInvokeDotNetFromJS, sendEndInvokeJSFromDotNet, sendByteArray, sendLocationChanged } from './Platform/WebView/WebViewIpcSender'; +import { fetchAndInvokeInitializers } from './JSInitializers/JSInitializers.WebView'; let started = false; @@ -13,6 +14,8 @@ async function boot(): Promise<void> { } started = true; + const jsInitializer = await fetchAndInvokeInitializers(); + startIpcReceiver(); DotNet.attachDispatcher({ @@ -25,6 +28,7 @@ async function boot(): Promise<void> { navigationManagerFunctions.listenForNavigationEvents(sendLocationChanged); sendAttachPage(navigationManagerFunctions.getBaseURI(), navigationManagerFunctions.getLocationHref()); + await jsInitializer.invokeAfterStartedCallbacks(Blazor); } Blazor.start = boot; diff --git a/src/Components/Web.JS/src/JSInitializers/JSInitializers.Server.ts b/src/Components/Web.JS/src/JSInitializers/JSInitializers.Server.ts new file mode 100644 index 00000000000..44a94169014 --- /dev/null +++ b/src/Components/Web.JS/src/JSInitializers/JSInitializers.Server.ts @@ -0,0 +1,16 @@ +import { BootJsonData } from "../Platform/BootConfig"; +import { CircuitStartOptions } from "../Platform/Circuits/CircuitStartOptions"; +import { JSInitializer } from "./JSInitializers"; + +export async function fetchAndInvokeInitializers(options: Partial<CircuitStartOptions>) : Promise<JSInitializer> { + const jsInitializersResponse = await fetch('_blazor/initializers', { + method: 'GET', + credentials: 'include', + cache: 'no-cache' + }); + + const initializers: string[] = await jsInitializersResponse.json(); + const jsInitializer = new JSInitializer(); + await jsInitializer.importInitializersAsync(initializers, [options]); + return jsInitializer; +} diff --git a/src/Components/Web.JS/src/JSInitializers/JSInitializers.WebAssembly.ts b/src/Components/Web.JS/src/JSInitializers/JSInitializers.WebAssembly.ts new file mode 100644 index 00000000000..23127e0b662 --- /dev/null +++ b/src/Components/Web.JS/src/JSInitializers/JSInitializers.WebAssembly.ts @@ -0,0 +1,15 @@ +import { BootJsonData } from "../Platform/BootConfig"; +import { WebAssemblyStartOptions } from "../Platform/WebAssemblyStartOptions"; +import { JSInitializer } from "./JSInitializers"; + +export async function fetchAndInvokeInitializers(bootConfig: BootJsonData, options: Partial<WebAssemblyStartOptions>) : Promise<JSInitializer> { + const initializers = bootConfig.resources.libraryInitializers; + const jsInitializer = new JSInitializer(); + if (initializers) { + await jsInitializer.importInitializersAsync( + Object.keys(initializers), + [options, bootConfig.resources.extensions]); + } + + return jsInitializer; +} diff --git a/src/Components/Web.JS/src/JSInitializers/JSInitializers.WebView.ts b/src/Components/Web.JS/src/JSInitializers/JSInitializers.WebView.ts new file mode 100644 index 00000000000..1cfd05092c4 --- /dev/null +++ b/src/Components/Web.JS/src/JSInitializers/JSInitializers.WebView.ts @@ -0,0 +1,14 @@ +import { JSInitializer } from "./JSInitializers"; + +export async function fetchAndInvokeInitializers() : Promise<JSInitializer> { + const jsInitializersResponse = await fetch('_framework/blazor.modules.json', { + method: 'GET', + credentials: 'include', + cache: 'no-cache' + }); + + const initializers: string[] = await jsInitializersResponse.json(); + const jsInitializer = new JSInitializer(); + await jsInitializer.importInitializersAsync(initializers, []); + return jsInitializer; +} diff --git a/src/Components/Web.JS/src/JSInitializers/JSInitializers.ts b/src/Components/Web.JS/src/JSInitializers/JSInitializers.ts new file mode 100644 index 00000000000..da0634357ff --- /dev/null +++ b/src/Components/Web.JS/src/JSInitializers/JSInitializers.ts @@ -0,0 +1,40 @@ +import { Blazor } from '../GlobalExports'; + +type BeforeBlazorStartedCallback = (...args: unknown[]) => Promise<void>; +export type AfterBlazorStartedCallback = (blazor: typeof Blazor) => Promise<void>; +type BlazorInitializer = { beforeStart: BeforeBlazorStartedCallback, afterStarted: AfterBlazorStartedCallback }; + +export class JSInitializer { + private afterStartedCallbacks: AfterBlazorStartedCallback[] = []; + + async importInitializersAsync(initializerFiles: string[], initializerArguments: unknown[]): Promise<void> { + await Promise.all(initializerFiles.map(f => importAndInvokeInitializer(this, f))); + + function adjustPath(path: string): string { + // This is the same we do in JS interop with the import callback + const base = document.baseURI; + path = base.endsWith('/') ? `${base}${path}` : `${base}/${path}`; + return path; + } + + async function importAndInvokeInitializer(jsInitializer: JSInitializer, path: string): Promise<void> { + const adjustedPath = adjustPath(path); + const initializer = await import(/* webpackIgnore: true */ adjustedPath) as Partial<BlazorInitializer>; + if (initializer === undefined) { + return; + } + const { beforeStart: beforeStart, afterStarted: afterStarted } = initializer; + if (afterStarted) { + jsInitializer.afterStartedCallbacks.push(afterStarted); + } + + if (beforeStart) { + return beforeStart(...initializerArguments); + } + } + } + + async invokeAfterStartedCallbacks(blazor: typeof Blazor) { + await Promise.all(this.afterStartedCallbacks.map(callback => callback(blazor))); + } +} diff --git a/src/Components/Web.JS/src/Platform/BootConfig.ts b/src/Components/Web.JS/src/Platform/BootConfig.ts index 6a0079e5f16..55ad90380d0 100644 --- a/src/Components/Web.JS/src/Platform/BootConfig.ts +++ b/src/Components/Web.JS/src/Platform/BootConfig.ts @@ -1,13 +1,20 @@ +import { WebAssemblyBootResourceType } from './WebAssemblyStartOptions'; + +type LoadBootResourceCallback = (type: WebAssemblyBootResourceType, name: string, defaultUri: string, integrity: string) => + string | Promise<Response> | null | undefined; + export class BootConfigResult { private constructor(public bootConfig: BootJsonData, public applicationEnvironment: string) { } - static async initAsync(environment?: string): Promise<BootConfigResult> { - const bootConfigResponse = await fetch('_framework/blazor.boot.json', { - method: 'GET', - credentials: 'include', - cache: 'no-cache' - }); + static async initAsync(loadBootResource?: LoadBootResourceCallback, environment?: string): Promise<BootConfigResult> { + const loaderResponse = loadBootResource !== undefined ? + loadBootResource('manifest', 'blazor.boot.json', '_framework/blazor.boot.json', '') : + defaultLoadBlazorBootJson('_framework/blazor.boot.json'); + + const bootConfigResponse = loaderResponse instanceof Promise ? + await loaderResponse : + await defaultLoadBlazorBootJson(loaderResponse ?? '_framework/blazor.boot.json'); // While we can expect an ASP.NET Core hosted application to include the environment, other // hosts may not. Assume 'Production' in the absence of any specified value. @@ -16,7 +23,16 @@ export class BootConfigResult { bootConfig.modifiableAssemblies = bootConfigResponse.headers.get('DOTNET-MODIFIABLE-ASSEMBLIES'); return new BootConfigResult(bootConfig, applicationEnvironment); + + async function defaultLoadBlazorBootJson(url: string) : Promise<Response> { + return fetch(url, { + method: 'GET', + credentials: 'include', + cache: 'no-cache' + }); + } }; + } // Keep in sync with bootJsonData from the BlazorWebAssemblySDK @@ -34,12 +50,16 @@ export interface BootJsonData { modifiableAssemblies: string | null; } +export type BootJsonDataExtension = { [extensionName: string]: ResourceList }; + export interface ResourceGroups { readonly assembly: ResourceList; readonly lazyAssembly: ResourceList; readonly pdb?: ResourceList; readonly runtime: ResourceList; - readonly satelliteResources?: { [cultureName: string] : ResourceList }; + readonly satelliteResources?: { [cultureName: string]: ResourceList }; + readonly libraryInitializers?: ResourceList, + readonly extensions?: BootJsonDataExtension } export type ResourceList = { [name: string]: string }; diff --git a/src/Components/Web.JS/src/Platform/WebAssemblyStartOptions.ts b/src/Components/Web.JS/src/Platform/WebAssemblyStartOptions.ts index ffacdbb11f4..c48a5100a5d 100644 --- a/src/Components/Web.JS/src/Platform/WebAssemblyStartOptions.ts +++ b/src/Components/Web.JS/src/Platform/WebAssemblyStartOptions.ts @@ -24,4 +24,4 @@ export interface WebAssemblyStartOptions { // This type doesn't have to align with anything in BootConfig. // Instead, this represents the public API through which certain aspects // of boot resource loading can be customized. -export type WebAssemblyBootResourceType = 'assembly' | 'pdb' | 'dotnetjs' | 'dotnetwasm' | 'globalization'; +export type WebAssemblyBootResourceType = 'assembly' | 'pdb' | 'dotnetjs' | 'dotnetwasm' | 'globalization' | 'manifest'; diff --git a/src/Components/Web.JS/tsconfig.json b/src/Components/Web.JS/tsconfig.json index a7813ded591..e1595b625df 100644 --- a/src/Components/Web.JS/tsconfig.json +++ b/src/Components/Web.JS/tsconfig.json @@ -6,6 +6,7 @@ "removeComments": false, "sourceMap": true, "target": "es2019", + "module": "es2020", "lib": ["es2019", "dom"], "strict": true } diff --git a/src/Components/Web/src/JSComponents/JSComponentInterop.cs b/src/Components/Web/src/JSComponents/JSComponentInterop.cs index c37466ceac2..1418ea5ab43 100644 --- a/src/Components/Web/src/JSComponents/JSComponentInterop.cs +++ b/src/Components/Web/src/JSComponents/JSComponentInterop.cs @@ -3,6 +3,7 @@ using System.Collections.Concurrent; using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Reflection.Metadata; using System.Text.Json; @@ -76,6 +77,8 @@ namespace Microsoft.AspNetCore.Components.Web.Infrastructure /// <summary> /// For framework use only. /// </summary> + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", + Justification = "OpenComponent already has the right set of attributes")] protected internal void SetRootComponentParameters(int componentId, int parameterCount, JsonElement parametersJson, JsonSerializerOptions jsonOptions) { // In case the client misreports the number of parameters, impose bounds so we know the amount diff --git a/src/Components/Web/src/WebEventData/WebEventData.cs b/src/Components/Web/src/WebEventData/WebEventData.cs index c8fc38f0add..01db2752e5c 100644 --- a/src/Components/Web/src/WebEventData/WebEventData.cs +++ b/src/Components/Web/src/WebEventData/WebEventData.cs @@ -59,6 +59,8 @@ namespace Microsoft.AspNetCore.Components.Web public EventArgs EventArgs { get; } + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", + Justification = "We are already using the appropriate overload")] private static EventArgs ParseEventArgsJson( Renderer renderer, JsonSerializerOptions jsonSerializerOptions, diff --git a/src/Components/WebAssembly/Samples/HostedBlazorWebassemblyApp/Server/Properties/launchSettings.json b/src/Components/WebAssembly/Samples/HostedBlazorWebassemblyApp/Server/Properties/launchSettings.json index 6ef6d4adc39..4b10fb01378 100644 --- a/src/Components/WebAssembly/Samples/HostedBlazorWebassemblyApp/Server/Properties/launchSettings.json +++ b/src/Components/WebAssembly/Samples/HostedBlazorWebassemblyApp/Server/Properties/launchSettings.json @@ -8,13 +8,6 @@ } }, "profiles": { - "IIS Express": { - "commandName": "IISExpress", - "launchBrowser": true, - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, "HostedBlazorWebassemblyApp.Server": { "commandName": "Project", "launchBrowser": true, @@ -22,6 +15,13 @@ "ASPNETCORE_ENVIRONMENT": "Development" }, "applicationUrl": "https://localhost:5001;http://localhost:5000" + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } } } -} \ No newline at end of file +} diff --git a/src/Components/WebAssembly/WebAssembly/src/HotReload/HotReloadAgent.cs b/src/Components/WebAssembly/WebAssembly/src/HotReload/HotReloadAgent.cs index db2817a9542..fcf1c19a2f9 100644 --- a/src/Components/WebAssembly/WebAssembly/src/HotReload/HotReloadAgent.cs +++ b/src/Components/WebAssembly/WebAssembly/src/HotReload/HotReloadAgent.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; using System.Reflection.Metadata; @@ -217,6 +218,8 @@ namespace Microsoft.Extensions.HotReload } } + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", + Justification = "OpenComponent already has the right set of attributes")] private Type[] GetMetadataUpdateTypes(IReadOnlyList<UpdateDelta> deltas) { List<Type>? types = null; diff --git a/src/Components/WebView/Samples/PhotinoPlatform/testassets/PhotinoTestApp/PhotinoTestApp.csproj b/src/Components/WebView/Samples/PhotinoPlatform/testassets/PhotinoTestApp/PhotinoTestApp.csproj index b9bdce6a5ea..2e581d2a73f 100644 --- a/src/Components/WebView/Samples/PhotinoPlatform/testassets/PhotinoTestApp/PhotinoTestApp.csproj +++ b/src/Components/WebView/Samples/PhotinoPlatform/testassets/PhotinoTestApp/PhotinoTestApp.csproj @@ -1,4 +1,4 @@ -<Project Sdk="Microsoft.NET.Sdk.Razor"> +<Project Sdk="Microsoft.NET.Sdk.Razor"> <PropertyGroup> <TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework> @@ -7,6 +7,8 @@ <SignAssembly>false</SignAssembly> </PropertyGroup> + <Import Project="..\..\..\..\WebView\src\buildTransitive\any\Microsoft.AspNetCore.Components.WebView.props" /> + <ItemGroup> <ProjectReference Include="..\..\src\Microsoft.AspNetCore.Components.WebView.Photino.csproj" /> <ProjectReference Include="..\..\..\..\..\test\testassets\BasicTestApp\BasicTestApp.csproj" /> diff --git a/src/Components/WebView/WebView/src/Microsoft.AspNetCore.Components.WebView.csproj b/src/Components/WebView/WebView/src/Microsoft.AspNetCore.Components.WebView.csproj index 075ed8d99f9..89f647db6b4 100644 --- a/src/Components/WebView/WebView/src/Microsoft.AspNetCore.Components.WebView.csproj +++ b/src/Components/WebView/WebView/src/Microsoft.AspNetCore.Components.WebView.csproj @@ -1,4 +1,4 @@ -<Project Sdk="Microsoft.NET.Sdk"> +<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework> @@ -26,11 +26,16 @@ <Compile Include="$(ComponentsSharedSourceRoot)src\ElementReferenceJsonConverter.cs" LinkBase="Shared" /> <Compile Include="$(ComponentsSharedSourceRoot)src\JsonSerializerOptionsProvider.cs" LinkBase="Shared" /> <Compile Include="$(SharedSourceRoot)LinkerFlags.cs" LinkBase="Shared" /> + <Compile Include="$(SharedSourceRoot)StaticWebAssets\**\*.cs" LinkBase="Shared" /> <Compile Include="$(ComponentsSharedSourceRoot)src\ArrayBuilder.cs" LinkBase="Shared" /> <Compile Include="$(ComponentsSharedSourceRoot)src\RenderBatchWriter.cs" LinkBase="Shared" /> <Compile Include="$(ComponentsSharedSourceRoot)src\ArrayBuilderMemoryStream.cs" LinkBase="Shared" /> </ItemGroup> + <ItemGroup> + <None Include="buildTransitive\any\Microsoft.AspNetCore.Components.WebView.props" Pack="true" PackagePath="%(Identity)" /> + </ItemGroup> + <ItemGroup> <Reference Include="Microsoft.AspNetCore.Components.Web" /> <Reference Include="Microsoft.Extensions.Configuration.Json" /> @@ -72,6 +77,7 @@ <Target Name="_AddEmbeddedBlazorWebView" BeforeTargets="_CalculateEmbeddedFilesManifestInputs" DependsOnTargets="_CheckBlazorWebViewJSPath"> <ItemGroup> + <EmbeddedResource Include="blazor.modules.json" LogicalName="_framework/blazor.modules.json" /> <EmbeddedResource Include="$(BlazorWebViewJSFile)" LogicalName="_framework/$(BlazorWebViewJSFilename)" /> <EmbeddedResource Include="$(BlazorWebViewJSFile).map" LogicalName="_framework/$(BlazorWebViewJSFilename).map" Condition="Exists('$(BlazorWebViewJSFile).map')" /> </ItemGroup> diff --git a/src/Components/WebView/WebView/src/StaticWebAssetsLoader.cs b/src/Components/WebView/WebView/src/StaticWebAssetsLoader.cs deleted file mode 100644 index f1202ba0a9b..00000000000 --- a/src/Components/WebView/WebView/src/StaticWebAssetsLoader.cs +++ /dev/null @@ -1,292 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -#nullable enable - -using System; -using System.Collections; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Xml.Linq; -using Microsoft.Extensions.FileProviders; -using Microsoft.Extensions.Primitives; - -namespace Microsoft.AspNetCore.Components.WebView -{ - internal class StaticWebAssetsLoader - { - internal const string StaticWebAssetsManifestName = "Microsoft.AspNetCore.StaticWebAssets.xml"; - - internal static IFileProvider UseStaticWebAssets(IFileProvider systemProvider) - { - using var manifest = GetManifestStream(); - if (manifest != null) - { - return UseStaticWebAssetsCore(systemProvider, manifest); - } - else - { - return systemProvider; - } - - static Stream? GetManifestStream() - { - try - { - var filePath = ResolveRelativeToAssembly(); - - if (filePath != null && File.Exists(filePath)) - { - return File.OpenRead(filePath); - } - else - { - // A missing manifest might simply mean that the feature is not enabled, so we simply - // return early. Misconfigurations will be uncommon given that the entire process is automated - // at build time. - return null; - } - } - catch - { - return null; - } - } - } - - internal static IFileProvider UseStaticWebAssetsCore(IFileProvider systemProvider, Stream manifest) - { - var webRootFileProvider = systemProvider; - - var additionalFiles = StaticWebAssetsReader.Parse(manifest) - .Select(cr => new StaticWebAssetsFileProvider(cr.BasePath, cr.Path)) - .OfType<IFileProvider>() // Upcast so we can insert on the resulting list. - .ToList(); - - if (additionalFiles.Count == 0) - { - return systemProvider; - } - else - { - additionalFiles.Insert(0, webRootFileProvider); - return new CompositeFileProvider(additionalFiles); - } - } - - private static string? ResolveRelativeToAssembly() - { - var assembly = Assembly.GetEntryAssembly(); - if (string.IsNullOrEmpty(assembly?.Location)) - { - return null; - } - - var name = Path.GetFileNameWithoutExtension(assembly.Location); - - return Path.Combine(Path.GetDirectoryName(assembly.Location)!, $"{name}.StaticWebAssets.xml"); - } - - internal static class StaticWebAssetsReader - { - private const string ManifestRootElementName = "StaticWebAssets"; - private const string VersionAttributeName = "Version"; - private const string ContentRootElementName = "ContentRoot"; - - internal static IEnumerable<ContentRootMapping> Parse(Stream manifest) - { - var document = XDocument.Load(manifest); - if (!string.Equals(document.Root!.Name.LocalName, ManifestRootElementName, StringComparison.OrdinalIgnoreCase)) - { - throw new InvalidOperationException($"Invalid manifest format. Manifest root must be '{ManifestRootElementName}'"); - } - - var version = document.Root.Attribute(VersionAttributeName); - if (version == null) - { - throw new InvalidOperationException($"Invalid manifest format. Manifest root element must contain a version '{VersionAttributeName}' attribute"); - } - - if (version.Value != "1.0") - { - throw new InvalidOperationException($"Unknown manifest version. Manifest version must be '1.0'"); - } - - foreach (var element in document.Root.Elements()) - { - if (!string.Equals(element.Name.LocalName, ContentRootElementName, StringComparison.OrdinalIgnoreCase)) - { - throw new InvalidOperationException($"Invalid manifest format. Invalid element '{element.Name.LocalName}'. All {StaticWebAssetsLoader.StaticWebAssetsManifestName} child elements must be '{ContentRootElementName}' elements."); - } - if (!element.IsEmpty) - { - throw new InvalidOperationException($"Invalid manifest format. {ContentRootElementName} can't have content."); - } - - var basePath = ParseRequiredAttribute(element, "BasePath"); - var path = ParseRequiredAttribute(element, "Path"); - yield return new ContentRootMapping(basePath, path); - } - } - - private static string ParseRequiredAttribute(XElement element, string attributeName) - { - var attribute = element.Attribute(attributeName); - if (attribute == null) - { - throw new InvalidOperationException($"Invalid manifest format. Missing {attributeName} attribute in '{ContentRootElementName}' element."); - } - return attribute.Value; - } - - internal readonly struct ContentRootMapping - { - public ContentRootMapping(string basePath, string path) - { - BasePath = basePath; - Path = path; - } - - public string BasePath { get; } - public string Path { get; } - } - } - - internal class StaticWebAssetsFileProvider : IFileProvider - { - private static readonly StringComparison FilePathComparison = OperatingSystem.IsWindows() ? - StringComparison.OrdinalIgnoreCase : - StringComparison.Ordinal; - - public StaticWebAssetsFileProvider(string pathPrefix, string contentRoot) - { - BasePath = NormalizePath(pathPrefix); - InnerProvider = new PhysicalFileProvider(contentRoot); - } - - public PhysicalFileProvider InnerProvider { get; } - - public PathString BasePath { get; } - - /// <inheritdoc /> - public IDirectoryContents GetDirectoryContents(string subpath) - { - var modifiedSub = NormalizePath(subpath); - - if (BasePath == "/") - { - return InnerProvider.GetDirectoryContents(modifiedSub); - } - - if (StartsWithBasePath(modifiedSub, out var physicalPath)) - { - return InnerProvider.GetDirectoryContents(physicalPath.Value); - } - else if (string.Equals(subpath, string.Empty) || string.Equals(modifiedSub, "/")) - { - return new StaticWebAssetsDirectoryRoot(BasePath); - } - else if (BasePath.StartsWithSegments(modifiedSub, FilePathComparison, out var remaining)) - { - return new StaticWebAssetsDirectoryRoot(remaining); - } - - return NotFoundDirectoryContents.Singleton; - } - - /// <inheritdoc /> - public IFileInfo GetFileInfo(string subpath) - { - var modifiedSub = NormalizePath(subpath); - - if (BasePath == "/") - { - return InnerProvider.GetFileInfo(subpath); - } - - if (!StartsWithBasePath(modifiedSub, out var physicalPath)) - { - return new NotFoundFileInfo(subpath); - } - else - { - return InnerProvider.GetFileInfo(physicalPath.Value); - } - } - - /// <inheritdoc /> - public IChangeToken Watch(string filter) - { - return InnerProvider.Watch(filter); - } - - private static string NormalizePath(string path) - { - path = path.Replace('\\', '/'); - return path.StartsWith('/') ? path : "/" + path; - } - - private bool StartsWithBasePath(string subpath, out PathString rest) - { - return new PathString(subpath).StartsWithSegments(BasePath, FilePathComparison, out rest); - } - - private class StaticWebAssetsDirectoryRoot : IDirectoryContents - { - private readonly string _nextSegment; - - public StaticWebAssetsDirectoryRoot(PathString remainingPath) - { - // We MUST use the Value property here because it is unescaped. - _nextSegment = remainingPath.Value?.Split("/", StringSplitOptions.RemoveEmptyEntries).FirstOrDefault() ?? string.Empty; - } - - public bool Exists => true; - - public IEnumerator<IFileInfo> GetEnumerator() - { - return GenerateEnum(); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return GenerateEnum(); - } - - private IEnumerator<IFileInfo> GenerateEnum() - { - return new[] { new StaticWebAssetsFileInfo(_nextSegment) } - .Cast<IFileInfo>().GetEnumerator(); - } - - private class StaticWebAssetsFileInfo : IFileInfo - { - public StaticWebAssetsFileInfo(string name) - { - Name = name; - } - - public bool Exists => true; - - public long Length => throw new NotImplementedException(); - - public string PhysicalPath => throw new NotImplementedException(); - - public DateTimeOffset LastModified => throw new NotImplementedException(); - - public bool IsDirectory => true; - - public string Name { get; } - - public Stream CreateReadStream() - { - throw new NotImplementedException(); - } - } - } - } - } -} -#nullable restore diff --git a/src/Components/WebView/WebView/src/WebViewManager.cs b/src/Components/WebView/WebView/src/WebViewManager.cs index 885473032c2..cc285129f61 100644 --- a/src/Components/WebView/WebView/src/WebViewManager.cs +++ b/src/Components/WebView/WebView/src/WebViewManager.cs @@ -4,12 +4,14 @@ using System; using System.Collections.Generic; using System.IO; +using System.Reflection; using System.Threading.Tasks; using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.Web.Infrastructure; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.FileProviders; using Microsoft.JSInterop; +using Microsoft.AspNetCore.StaticWebAssets; namespace Microsoft.AspNetCore.Components.WebView { @@ -244,5 +246,38 @@ namespace Microsoft.AspNetCore.Components.WebView GC.SuppressFinalize(this); await DisposeAsyncCore(); } + + private class StaticWebAssetsLoader + { + internal static IFileProvider UseStaticWebAssets(IFileProvider fileProvider) + { + var manifestPath = ResolveRelativeToAssembly(); + if (File.Exists(manifestPath)) + { + using var manifestStream = File.OpenRead(manifestPath); + var manifest = ManifestStaticWebAssetFileProvider.StaticWebAssetManifest.Parse(manifestStream); + if (manifest.ContentRoots.Length > 0) + { + var manifestProvider = new ManifestStaticWebAssetFileProvider(manifest, (path) => new PhysicalFileProvider(path)); + return new CompositeFileProvider(manifestProvider, fileProvider); + } + } + + return fileProvider; + } + + private static string? ResolveRelativeToAssembly() + { + var assembly = Assembly.GetEntryAssembly(); + if (string.IsNullOrEmpty(assembly?.Location)) + { + return null; + } + + var name = Path.GetFileNameWithoutExtension(assembly.Location); + + return Path.Combine(Path.GetDirectoryName(assembly.Location)!, $"{name}.staticwebassets.runtime.json"); + } + } } } diff --git a/src/Components/WebView/WebView/src/blazor.modules.json b/src/Components/WebView/WebView/src/blazor.modules.json new file mode 100644 index 00000000000..fe51488c706 --- /dev/null +++ b/src/Components/WebView/WebView/src/blazor.modules.json @@ -0,0 +1 @@ +[] diff --git a/src/Components/WebView/WebView/src/buildTransitive/any/Microsoft.AspNetCore.Components.WebView.props b/src/Components/WebView/WebView/src/buildTransitive/any/Microsoft.AspNetCore.Components.WebView.props new file mode 100644 index 00000000000..f43c2047fb3 --- /dev/null +++ b/src/Components/WebView/WebView/src/buildTransitive/any/Microsoft.AspNetCore.Components.WebView.props @@ -0,0 +1,5 @@ +<Project> + <PropertyGroup> + <JSModuleManifestRelativePath>_framework/blazor.modules.json</JSModuleManifestRelativePath> + </PropertyGroup> +</Project> diff --git a/src/Components/test/E2ETest/ServerExecutionTests/JsInitializersTest.cs b/src/Components/test/E2ETest/ServerExecutionTests/JsInitializersTest.cs new file mode 100644 index 00000000000..d39e70a2d6a --- /dev/null +++ b/src/Components/test/E2ETest/ServerExecutionTests/JsInitializersTest.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using BasicTestApp; +using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures; +using Microsoft.AspNetCore.Components.E2ETest.Tests; +using Microsoft.AspNetCore.E2ETesting; +using OpenQA.Selenium; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests +{ + public class ServerJsInitializersTest : JsInitializersTest + { + public ServerJsInitializersTest( + BrowserFixture browserFixture, + ToggleExecutionModeServerFixture<Program> serverFixture, + ITestOutputHelper output) + : base(browserFixture, serverFixture.WithServerExecution(), output) + { + } + } +} diff --git a/src/Components/test/E2ETest/Tests/JsInitializersTest.cs b/src/Components/test/E2ETest/Tests/JsInitializersTest.cs new file mode 100644 index 00000000000..e8fc1371596 --- /dev/null +++ b/src/Components/test/E2ETest/Tests/JsInitializersTest.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using BasicTestApp; +using Microsoft.AspNetCore.Components.E2ETest.Infrastructure; +using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures; +using Microsoft.AspNetCore.E2ETesting; +using OpenQA.Selenium; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.AspNetCore.Components.E2ETest.Tests +{ + public class JsInitializersTest : ServerTestBase<ToggleExecutionModeServerFixture<Program>> + { + public JsInitializersTest( + BrowserFixture browserFixture, + ToggleExecutionModeServerFixture<Program> serverFixture, + ITestOutputHelper output) + : base(browserFixture, serverFixture, output) + { + } + + protected override void InitializeAsyncCore() + { + Navigate(ServerPathBase + "#initializer"); + } + + [Fact] + public void InitializersWork() + { + Browser.Exists(By.Id("initializer-start")); + Browser.Exists(By.Id("initializer-end")); + } + + [Fact] + public void CanLoadJsModulePackagesFromLibrary() + { + Browser.MountTestComponent<ExternalContentPackage>(); + Browser.Equal<string>("Hello from module", () => Browser.Exists(By.CssSelector(".js-module-message > p")).Text); + } + } +} diff --git a/src/Components/test/testassets/BasicTestApp/wwwroot/BasicTestApp.lib.module.js b/src/Components/test/testassets/BasicTestApp/wwwroot/BasicTestApp.lib.module.js new file mode 100644 index 00000000000..d0e8652a3c4 --- /dev/null +++ b/src/Components/test/testassets/BasicTestApp/wwwroot/BasicTestApp.lib.module.js @@ -0,0 +1,48 @@ +let runInitializer = false; +let resourceRequests = []; +export async function beforeStart(options) { + const url = new URL(document.URL); + runInitializer = url.hash.indexOf('initializer') !== -1; + if (runInitializer) { + if (!options.logLevel) { + // Simple way of detecting we are in web assembly + options.loadBootResource = function (type, name, defaultUri, integrity) { + resourceRequests.push([type, name, defaultUri, integrity]); + return defaultUri; + } + } + const start = document.createElement('p'); + start.innerText = 'Before starting'; + start.style = 'background-color: green; color: white'; + start.setAttribute('id', 'initializer-start'); + document.body.appendChild(start); + } +} + +export async function afterStarted() { + if (runInitializer) { + const end = document.createElement('p'); + end.setAttribute('id', 'initializer-end'); + end.innerText = 'After started'; + end.style = 'background-color: pink'; + document.body.appendChild(end); + + if (resourceRequests.length > 0) { + + const resourceRow = (row) => `<tr><td>${row[0]}</td><td>${row[1]}</td><td>${row[2]}</td><td>${row[3]}</td></tr>`; + const rows = resourceRequests.reduce((previewRows, currentRow) => previewRows + resourceRow(currentRow), ''); + + const requestTable = document.createElement('table'); + requestTable.setAttribute('id', 'total-requests'); + requestTable.innerHTML = `<tr> + <th>type</th> + <th>name</th> + <th>default-uri</th> + <th>integrity</th> +</tr> +${rows} +`; + document.body.appendChild(requestTable); + } + } +} diff --git a/src/Components/test/testassets/TestContentPackage/ComponentFromPackage.razor b/src/Components/test/testassets/TestContentPackage/ComponentFromPackage.razor index 22c9c43e829..553de9db700 100644 --- a/src/Components/test/testassets/TestContentPackage/ComponentFromPackage.razor +++ b/src/Components/test/testassets/TestContentPackage/ComponentFromPackage.razor @@ -1,11 +1,24 @@ -<div class="special-style"> +@using Microsoft.JSInterop +@inject IJSRuntime JS + +<div class="special-style"> This component, including the CSS and image required to produce its elegant styling, is in an external NuGet package. <button @onclick="ChangeLabel">@buttonLabel </button> </div> +<div class="js-module-message"> +</div> + @code { string buttonLabel = "Click me"; + private IJSObjectReference _module = null; + + protected async override Task OnInitializedAsync() + { + _module = await JS.InvokeAsync<IJSObjectReference>("import", "./_content/TestContentPackage/ComponentFromPackage.razor.js"); + await _module.InvokeVoidAsync("displayMessage", "Hello from module"); + } void ChangeLabel() { diff --git a/src/Components/test/testassets/TestContentPackage/ComponentFromPackage.razor.js b/src/Components/test/testassets/TestContentPackage/ComponentFromPackage.razor.js new file mode 100644 index 00000000000..965a355da10 --- /dev/null +++ b/src/Components/test/testassets/TestContentPackage/ComponentFromPackage.razor.js @@ -0,0 +1,5 @@ +export function displayMessage(message) { + const element = document.createElement("p"); + element.innerText = message; + document.querySelector('.js-module-message').appendChild(element); +} diff --git a/src/Hosting/Hosting/src/Microsoft.AspNetCore.Hosting.csproj b/src/Hosting/Hosting/src/Microsoft.AspNetCore.Hosting.csproj index d6478259374..107e1d40c68 100644 --- a/src/Hosting/Hosting/src/Microsoft.AspNetCore.Hosting.csproj +++ b/src/Hosting/Hosting/src/Microsoft.AspNetCore.Hosting.csproj @@ -14,6 +14,7 @@ <Compile Include="$(SharedSourceRoot)RazorViews\*.cs" /> <Compile Include="$(SharedSourceRoot)StackTrace\**\*.cs" /> <Compile Include="$(SharedSourceRoot)ErrorPage\**\*.cs" /> + <Compile Include="$(SharedSourceRoot)StaticWebAssets\**\*.cs" /> </ItemGroup> <ItemGroup> diff --git a/src/Hosting/Hosting/src/StaticWebAssets/StaticWebAssetsLoader.cs b/src/Hosting/Hosting/src/StaticWebAssets/StaticWebAssetsLoader.cs index 483fff518cf..f353900b93e 100644 --- a/src/Hosting/Hosting/src/StaticWebAssets/StaticWebAssetsLoader.cs +++ b/src/Hosting/Hosting/src/StaticWebAssets/StaticWebAssetsLoader.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Reflection; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.FileProviders; +using Microsoft.AspNetCore.StaticWebAssets; namespace Microsoft.AspNetCore.Hosting.StaticWebAssets { @@ -16,8 +17,6 @@ namespace Microsoft.AspNetCore.Hosting.StaticWebAssets /// </summary> public class StaticWebAssetsLoader { - internal const string StaticWebAssetsManifestName = "Microsoft.AspNetCore.StaticWebAssets.xml"; - /// <summary> /// Configure the <see cref="IWebHostEnvironment"/> to use static web assets. /// </summary> @@ -25,61 +24,34 @@ namespace Microsoft.AspNetCore.Hosting.StaticWebAssets /// <param name="configuration">The host <see cref="IConfiguration"/>.</param> public static void UseStaticWebAssets(IWebHostEnvironment environment, IConfiguration configuration) { - var (manifest, isJson) = ResolveManifest(environment, configuration); - using (manifest) + var manifest = ResolveManifest(environment, configuration); + if (manifest != null) { - if (manifest != null) + using (manifest) { - UseStaticWebAssetsCore(environment, manifest, isJson); + UseStaticWebAssetsCore(environment, manifest); } } } - internal static void UseStaticWebAssetsCore(IWebHostEnvironment environment, Stream manifest, bool isJson) + internal static void UseStaticWebAssetsCore(IWebHostEnvironment environment, Stream manifest) { - if (isJson) - { - var staticWebAssetManifest = ManifestStaticWebAssetFileProvider.StaticWebAssetManifest.Parse(manifest); - var provider = new ManifestStaticWebAssetFileProvider( - staticWebAssetManifest, - (contentRoot) => new PhysicalFileProvider(contentRoot)); - - environment.WebRootFileProvider = new CompositeFileProvider(new[] { environment.WebRootFileProvider, provider }); - return; - } - - var webRootFileProvider = environment.WebRootFileProvider; - - var additionalFiles = StaticWebAssetsReader.Parse(manifest) - .Select(cr => new StaticWebAssetsFileProvider(cr.BasePath, cr.Path)) - .OfType<IFileProvider>() // Upcast so we can insert on the resulting list. - .ToList(); + var staticWebAssetManifest = ManifestStaticWebAssetFileProvider.StaticWebAssetManifest.Parse(manifest); + var provider = new ManifestStaticWebAssetFileProvider( + staticWebAssetManifest, + (contentRoot) => new PhysicalFileProvider(contentRoot)); - if (additionalFiles.Count == 0) - { - return; - } - else - { - additionalFiles.Insert(0, webRootFileProvider); - environment.WebRootFileProvider = new CompositeFileProvider(additionalFiles); - } + environment.WebRootFileProvider = new CompositeFileProvider(new[] { provider, environment.WebRootFileProvider }); } - internal static (Stream, bool) ResolveManifest(IWebHostEnvironment environment, IConfiguration configuration) + internal static Stream? ResolveManifest(IWebHostEnvironment environment, IConfiguration configuration) { try { - var manifestPath = configuration.GetValue<string>(WebHostDefaults.StaticWebAssetsKey); - var isJson = manifestPath != null && Path.GetExtension(manifestPath).EndsWith(".json", OperatingSystem.IsWindows() ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal); - var candidates = manifestPath != null ? new[] { (manifestPath, isJson) } : ResolveRelativeToAssembly(environment); - - foreach (var (candidate, json) in candidates) + var candidate = configuration.GetValue<string>(WebHostDefaults.StaticWebAssetsKey) ?? ResolveRelativeToAssembly(environment); + if (candidate != null && File.Exists(candidate)) { - if (candidate != null && File.Exists(candidate)) - { - return (File.OpenRead(candidate), json); - } + return File.OpenRead(candidate); } // A missing manifest might simply mean that the feature is not enabled, so we simply @@ -93,12 +65,11 @@ namespace Microsoft.AspNetCore.Hosting.StaticWebAssets } } - private static IEnumerable<(string candidatePath, bool isJson)> ResolveRelativeToAssembly(IWebHostEnvironment environment) + private static string ResolveRelativeToAssembly(IWebHostEnvironment environment) { var assembly = Assembly.Load(environment.ApplicationName); var basePath = string.IsNullOrEmpty(assembly.Location) ? AppContext.BaseDirectory : Path.GetDirectoryName(assembly.Location); - yield return (Path.Combine(basePath!, $"{environment.ApplicationName}.staticwebassets.runtime.json"), isJson: true); - yield return (Path.Combine(basePath!, $"{environment.ApplicationName}.StaticWebAssets.xml"), isJson: false); + return Path.Combine(basePath!, $"{environment.ApplicationName}.staticwebassets.runtime.json"); } } } diff --git a/src/Hosting/Hosting/src/StaticWebAssets/StaticWebAssetsReader.cs b/src/Hosting/Hosting/src/StaticWebAssets/StaticWebAssetsReader.cs deleted file mode 100644 index 6f1bae0b682..00000000000 --- a/src/Hosting/Hosting/src/StaticWebAssets/StaticWebAssetsReader.cs +++ /dev/null @@ -1,75 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Generic; -using System.IO; -using System.Xml.Linq; - -namespace Microsoft.AspNetCore.Hosting.StaticWebAssets -{ - internal static class StaticWebAssetsReader - { - private const string ManifestRootElementName = "StaticWebAssets"; - private const string VersionAttributeName = "Version"; - private const string ContentRootElementName = "ContentRoot"; - - internal static IEnumerable<ContentRootMapping> Parse(Stream manifest) - { - var document = XDocument.Load(manifest); - if (!string.Equals(document.Root!.Name.LocalName, ManifestRootElementName, StringComparison.OrdinalIgnoreCase)) - { - throw new InvalidOperationException($"Invalid manifest format. Manifest root must be '{ManifestRootElementName}'"); - } - - var version = document.Root.Attribute(VersionAttributeName); - if (version == null) - { - throw new InvalidOperationException($"Invalid manifest format. Manifest root element must contain a version '{VersionAttributeName}' attribute"); - } - - if (version.Value != "1.0") - { - throw new InvalidOperationException($"Unknown manifest version. Manifest version must be '1.0'"); - } - - foreach (var element in document.Root.Elements()) - { - if (!string.Equals(element.Name.LocalName, ContentRootElementName, StringComparison.OrdinalIgnoreCase)) - { - throw new InvalidOperationException($"Invalid manifest format. Invalid element '{element.Name.LocalName}'. All {StaticWebAssetsLoader.StaticWebAssetsManifestName} child elements must be '{ContentRootElementName}' elements."); - } - if (!element.IsEmpty) - { - throw new InvalidOperationException($"Invalid manifest format. {ContentRootElementName} can't have content."); - } - - var basePath = ParseRequiredAttribute(element, "BasePath"); - var path = ParseRequiredAttribute(element, "Path"); - yield return new ContentRootMapping(basePath, path); - } - } - - private static string ParseRequiredAttribute(XElement element, string attributeName) - { - var attribute = element.Attribute(attributeName); - if (attribute == null) - { - throw new InvalidOperationException($"Invalid manifest format. Missing {attributeName} attribute in '{ContentRootElementName}' element."); - } - return attribute.Value; - } - - internal readonly struct ContentRootMapping - { - public ContentRootMapping(string basePath, string path) - { - BasePath = basePath; - Path = path; - } - - public string BasePath { get; } - public string Path { get; } - } - } -} diff --git a/src/Hosting/Hosting/test/StaticWebAssets/ManifestStaticWebAssetsFileProviderTests.cs b/src/Hosting/Hosting/test/StaticWebAssets/ManifestStaticWebAssetsFileProviderTests.cs index 198ce8932f0..24f537afc6e 100644 --- a/src/Hosting/Hosting/test/StaticWebAssets/ManifestStaticWebAssetsFileProviderTests.cs +++ b/src/Hosting/Hosting/test/StaticWebAssets/ManifestStaticWebAssetsFileProviderTests.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Hosting.StaticWebAssets; +using Microsoft.AspNetCore.StaticWebAssets; using Microsoft.Extensions.FileProviders; using Moq; using Xunit; @@ -23,7 +24,7 @@ namespace Microsoft.AspNetCore.Hosting.Tests.StaticWebAssets var comparer = ManifestStaticWebAssetFileProvider.StaticWebAssetManifest.PathComparer; var expectedResult = OperatingSystem.IsWindows(); var manifest = new ManifestStaticWebAssetFileProvider.StaticWebAssetManifest(); - manifest.ContentRoots = new[] { Path.GetDirectoryName(typeof(StaticWebAssetsFileProviderTests).Assembly.Location) }; + manifest.ContentRoots = new[] { Path.GetDirectoryName(typeof(ManifestStaticWebAssetsFileProviderTest).Assembly.Location) }; manifest.Root = new() { Children = new(comparer) @@ -360,7 +361,7 @@ namespace Microsoft.AspNetCore.Hosting.Tests.StaticWebAssets var comparer = ManifestStaticWebAssetFileProvider.StaticWebAssetManifest.PathComparer; var expectedResult = OperatingSystem.IsWindows(); var manifest = new ManifestStaticWebAssetFileProvider.StaticWebAssetManifest(); - manifest.ContentRoots = new[] { Path.GetDirectoryName(typeof(StaticWebAssetsFileProviderTests).Assembly.Location) }; + manifest.ContentRoots = new[] { Path.GetDirectoryName(typeof(ManifestStaticWebAssetsFileProviderTest).Assembly.Location) }; manifest.Root = new() { Children = new(comparer) diff --git a/src/Hosting/Hosting/test/StaticWebAssets/StaticWebAssetsFileProviderTests.cs b/src/Hosting/Hosting/test/StaticWebAssets/StaticWebAssetsFileProviderTests.cs deleted file mode 100644 index fa13a43c497..00000000000 --- a/src/Hosting/Hosting/test/StaticWebAssets/StaticWebAssetsFileProviderTests.cs +++ /dev/null @@ -1,210 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.IO; -using Xunit; - -namespace Microsoft.AspNetCore.Hosting.StaticWebAssets -{ - public class StaticWebAssetsFileProviderTests - { - [Fact] - public void StaticWebAssetsFileProvider_ConstructorThrows_WhenPathIsNotFound() - { - // Arrange, Act & Assert - var provider = Assert.Throws<DirectoryNotFoundException>(() => new StaticWebAssetsFileProvider("/prefix", "/nonexisting")); - } - - [Fact] - public void StaticWebAssetsFileProvider_Constructor_PrependsPrefixWithSlashIfMissing() - { - // Arrange & Act - var provider = new StaticWebAssetsFileProvider("_content", AppContext.BaseDirectory); - - // Assert - Assert.Equal("/_content", provider.BasePath); - } - - [Fact] - public void StaticWebAssetsFileProvider_Constructor_DoesNotPrependPrefixWithSlashIfPresent() - { - // Arrange & Act - var provider = new StaticWebAssetsFileProvider("/_content", AppContext.BaseDirectory); - - // Assert - Assert.Equal("/_content", provider.BasePath); - } - - [Theory] - [InlineData("\\", "_content")] - [InlineData("\\_content\\RazorClassLib\\Dir", "Castle.Core.dll")] - [InlineData("", "_content")] - [InlineData("/", "_content")] - [InlineData("/_content", "RazorClassLib")] - [InlineData("/_content/RazorClassLib", "Dir")] - [InlineData("/_content/RazorClassLib/Dir", "Microsoft.AspNetCore.Hosting.Tests.dll")] - [InlineData("/_content/RazorClassLib/Dir/testroot/", "TextFile.txt")] - [InlineData("/_content/RazorClassLib/Dir/testroot/wwwroot/", "README")] - public void GetDirectoryContents_WalksUpContentRoot(string searchDir, string expected) - { - // Arrange - var provider = new StaticWebAssetsFileProvider("/_content/RazorClassLib/Dir", AppContext.BaseDirectory); - - // Act - var directory = provider.GetDirectoryContents(searchDir); - - // Assert - Assert.NotEmpty(directory); - Assert.Contains(directory, file => string.Equals(file.Name, expected)); - } - - [Fact] - public void GetDirectoryContents_DoesNotFindNonExistentFiles() - { - // Arrange - var provider = new StaticWebAssetsFileProvider("/_content/RazorClassLib/", AppContext.BaseDirectory); - - // Act - var directory = provider.GetDirectoryContents("/_content/RazorClassLib/False"); - - // Assert - Assert.Empty(directory); - } - - [Theory] - [InlineData("/False/_content/RazorClassLib/")] - [InlineData("/_content/RazorClass")] - public void GetDirectoryContents_PartialMatchFails(string requestedUrl) - { - // Arrange - var provider = new StaticWebAssetsFileProvider("/_content/RazorClassLib", AppContext.BaseDirectory); - - // Act - var directory = provider.GetDirectoryContents(requestedUrl); - - // Assert - Assert.Empty(directory); - } - - [Fact] - public void GetDirectoryContents_HandlersEmptyPath() - { - // Arrange - var provider = new StaticWebAssetsFileProvider("/_content", - Path.Combine(AppContext.BaseDirectory, "testroot", "wwwroot")); - - // Act - var directory = provider.GetDirectoryContents(""); - - // Assert - Assert.True(directory.Exists); - } - - [Fact] - public void GetDirectoryContents_HandlesWhitespaceInBase() - { - // Arrange - var provider = new StaticWebAssetsFileProvider("/_content/Static Web Assets", - Path.Combine(AppContext.BaseDirectory, "testroot", "wwwroot")); - - // Act - var directory = provider.GetDirectoryContents("/_content/Static Web Assets/Static Web/"); - - // Assert - Assert.Collection(directory, - file => - { - Assert.Equal("Static Web.txt", file.Name); - }); - } - - [Fact] - public void StaticWebAssetsFileProvider_FindsFileWithSpaces() - { - // Arrange & Act - var provider = new StaticWebAssetsFileProvider("/_content", - Path.Combine(AppContext.BaseDirectory, "testroot", "wwwroot")); - - // Assert - Assert.True(provider.GetFileInfo("/_content/Static Web Assets.txt").Exists); - } - - [Fact] - public void GetDirectoryContents_HandlesEmptyBasePath() - { - // Arrange - var provider = new StaticWebAssetsFileProvider("/", - Path.Combine(AppContext.BaseDirectory, "testroot", "wwwroot")); - - // Act - var directory = provider.GetDirectoryContents("/Static Web/"); - - // Assert - Assert.Collection(directory, - file => - { - Assert.Equal("Static Web.txt", file.Name); - }); - } - - [Fact] - public void StaticWebAssetsFileProviderWithEmptyBasePath_FindsFile() - { - // Arrange & Act - var provider = new StaticWebAssetsFileProvider("/", - Path.Combine(AppContext.BaseDirectory, "testroot", "wwwroot")); - - // Assert - Assert.True(provider.GetFileInfo("/Static Web Assets.txt").Exists); - } - - [Fact] - public void GetFileInfo_DoesNotMatch_IncompletePrefixSegments() - { - // Arrange - var expectedResult = OperatingSystem.IsWindows(); - var provider = new StaticWebAssetsFileProvider( - "_cont", - Path.GetDirectoryName(typeof(StaticWebAssetsFileProviderTests).Assembly.Location)); - - // Act - var file = provider.GetFileInfo("/_content/Microsoft.AspNetCore.TestHost.StaticWebAssets.xml"); - - // Assert - Assert.False(file.Exists, "File exists"); - } - - [Fact] - public void GetFileInfo_Prefix_RespectsOsCaseSensitivity() - { - // Arrange - var expectedResult = OperatingSystem.IsWindows(); - var provider = new StaticWebAssetsFileProvider( - "_content", - Path.GetDirectoryName(typeof(StaticWebAssetsFileProviderTests).Assembly.Location)); - - // Act - var file = provider.GetFileInfo("/_CONTENT/Microsoft.AspNetCore.Hosting.StaticWebAssets.xml"); - - // Assert - Assert.Equal(expectedResult, file.Exists); - } - - [Fact] - public void GetDirectoryContents_Prefix_RespectsOsCaseSensitivity() - { - // Arrange - var expectedResult = OperatingSystem.IsWindows(); - var provider = new StaticWebAssetsFileProvider( - "_content", - Path.GetDirectoryName(typeof(StaticWebAssetsFileProviderTests).Assembly.Location)); - - // Act - var directory = provider.GetDirectoryContents("/_CONTENT"); - - // Assert - Assert.Equal(expectedResult, directory.Exists); - } - } -} diff --git a/src/Hosting/Hosting/test/StaticWebAssets/StaticWebAssetsLoaderTests.cs b/src/Hosting/Hosting/test/StaticWebAssets/StaticWebAssetsLoaderTests.cs deleted file mode 100644 index f714b344071..00000000000 --- a/src/Hosting/Hosting/test/StaticWebAssets/StaticWebAssetsLoaderTests.cs +++ /dev/null @@ -1,116 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.FileProviders; -using Microsoft.Extensions.Hosting; -using Xunit; - -namespace Microsoft.AspNetCore.Hosting.StaticWebAssets -{ - public class StaticWebAssetsLoaderTests - { - [Fact] - public void UseStaticWebAssetsCore_CreatesCompositeRoot_WhenThereAreContentRootsInTheManifest() - { - // Arrange - var manifestContent = @$"<StaticWebAssets Version=""1.0""> - <ContentRoot Path=""{AppContext.BaseDirectory}"" BasePath=""/BasePath"" /> -</StaticWebAssets>"; - - var manifest = CreateManifest(manifestContent); - var originalRoot = new NullFileProvider(); - var environment = new HostingEnvironment() - { - WebRootFileProvider = originalRoot - }; - - // Act - StaticWebAssetsLoader.UseStaticWebAssetsCore(environment, manifest, false); - - // Assert - var composite = Assert.IsType<CompositeFileProvider>(environment.WebRootFileProvider); - Assert.Equal(2, composite.FileProviders.Count()); - Assert.Equal(originalRoot, composite.FileProviders.First()); - } - - [Fact] - public void UseStaticWebAssetsCore_DoesNothing_WhenManifestDoesNotContainEntries() - { - // Arrange - var manifestContent = @$"<StaticWebAssets Version=""1.0""> -</StaticWebAssets>"; - - var manifest = CreateManifest(manifestContent); - var originalRoot = new NullFileProvider(); - var environment = new HostingEnvironment() - { - WebRootFileProvider = originalRoot - }; - - // Act - StaticWebAssetsLoader.UseStaticWebAssetsCore(environment, manifest, false); - - // Assert - Assert.Equal(originalRoot, environment.WebRootFileProvider); - } - - [Fact] - public void ResolveManifest_ManifestFromFile() - { - // Arrange - var expectedManifest = @"<StaticWebAssets Version=""1.0""> - <ContentRoot Path=""/Path"" BasePath=""/BasePath"" /> -</StaticWebAssets> -"; - - var environment = new HostingEnvironment() - { - ApplicationName = "Microsoft.AspNetCore.Hosting" - }; - - // Act - var (manifest,_) = StaticWebAssetsLoader.ResolveManifest(environment, new ConfigurationBuilder().Build()); - - // Assert - Assert.Equal(expectedManifest, new StreamReader(manifest).ReadToEnd()); - } - - [Fact] - public void ResolveManifest_UsesConfigurationKey_WhenProvided() - { - // Arrange - var expectedManifest = @"<StaticWebAssets Version=""1.0""> - <ContentRoot Path=""/Path"" BasePath=""/BasePath"" /> -</StaticWebAssets> -"; - var path = Path.ChangeExtension(typeof(StaticWebAssetsLoader).Assembly.Location, ".StaticWebAssets.xml"); - var environment = new HostingEnvironment() - { - ApplicationName = "NonExistingDll" - }; - - var configuration = new ConfigurationBuilder() - .AddInMemoryCollection(new Dictionary<string, string>() { - [WebHostDefaults.StaticWebAssetsKey] = path - }).Build(); - - // Act - var (manifest,_) = StaticWebAssetsLoader.ResolveManifest(environment, configuration); - - // Assert - Assert.Equal(expectedManifest, new StreamReader(manifest).ReadToEnd()); - } - - - private Stream CreateManifest(string manifestContent) - { - return new MemoryStream(Encoding.UTF8.GetBytes(manifestContent)); - } - } -} diff --git a/src/Identity/test/Identity.FunctionalTests/Infrastructure/ServerFactory.cs b/src/Identity/test/Identity.FunctionalTests/Infrastructure/ServerFactory.cs index 912e817dca9..de41e17e4ad 100644 --- a/src/Identity/test/Identity.FunctionalTests/Infrastructure/ServerFactory.cs +++ b/src/Identity/test/Identity.FunctionalTests/Infrastructure/ServerFactory.cs @@ -57,71 +57,12 @@ namespace Microsoft.AspNetCore.Identity.FunctionalTests .AddCookieTempDataProvider(o => o.Cookie.IsEssential = true); }); - UpdateStaticAssets(builder); UpdateApplicationParts(builder); } private void UpdateApplicationParts(IWebHostBuilder builder) => builder.ConfigureServices(services => AddRelatedParts(services, BootstrapFrameworkVersion)); - private void UpdateStaticAssets(IWebHostBuilder builder) - { - var manifestPath = Path.GetDirectoryName(typeof(ServerFactory<,>).Assembly.Location); - builder.ConfigureAppConfiguration((ctx, cb) => - { - if (ctx.HostingEnvironment.WebRootFileProvider is CompositeFileProvider composite) - { - var originalWebRoot = composite.FileProviders.First(); - ctx.HostingEnvironment.WebRootFileProvider = originalWebRoot; - } - }); - - string versionedPath = Path.Combine(manifestPath, $"Testing.DefaultWebSite.StaticWebAssets.{BootstrapFrameworkVersion}.xml"); - UpdateManifest(versionedPath); - - builder.ConfigureAppConfiguration((context, configBuilder) => - { - using (var manifest = File.OpenRead(versionedPath)) - { - typeof(StaticWebAssetsLoader) - .GetMethod("UseStaticWebAssetsCore", BindingFlags.NonPublic | BindingFlags.Static) - .Invoke(null, new object[] { context.HostingEnvironment, manifest, false }); - } - }); - } - - private void UpdateManifest(string versionedPath) - { - var content = File.ReadAllText(versionedPath); - var path = typeof(ServerFactory<,>).Assembly.GetCustomAttributes<AssemblyMetadataAttribute>() - .Single(a => a.Key == "Microsoft.AspNetCore.Testing.IdentityUIProjectPath").Value; - - path = Directory.Exists(path) ? Path.Combine(path, "wwwroot") : Path.Combine(FindHelixSlnFileDirectory(), "UI", "wwwroot"); - - var updatedContent = content.Replace("{TEST_PLACEHOLDER}", path); - - File.WriteAllText(versionedPath, updatedContent); - } - - private string FindHelixSlnFileDirectory() - { - var applicationPath = Path.GetDirectoryName(typeof(ServerFactory<,>).Assembly.Location); - var directoryInfo = new DirectoryInfo(applicationPath); - do - { - var solutionPath = Directory.EnumerateFiles(directoryInfo.FullName, "*.sln").FirstOrDefault(); - if (solutionPath != null) - { - return directoryInfo.FullName; - } - - directoryInfo = directoryInfo.Parent; - } - while (directoryInfo.Parent != null); - - throw new InvalidOperationException($"Solution root could not be located using application root {applicationPath}."); - } - protected override IHost CreateHost(IHostBuilder builder) { var result = base.CreateHost(builder); diff --git a/src/Hosting/Hosting/src/StaticWebAssets/StaticWebAssetsFileProvider.cs b/src/Shared/StaticWebAssets/StaticWebAssetsFileProvider.cs similarity index 74% rename from src/Hosting/Hosting/src/StaticWebAssets/StaticWebAssetsFileProvider.cs rename to src/Shared/StaticWebAssets/StaticWebAssetsFileProvider.cs index fd8b8cffa58..a5a5ed15598 100644 --- a/src/Hosting/Hosting/src/StaticWebAssets/StaticWebAssetsFileProvider.cs +++ b/src/Shared/StaticWebAssets/StaticWebAssetsFileProvider.cs @@ -6,159 +6,12 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text.Json; using System.Text.Json.Serialization; -using Microsoft.AspNetCore.Http; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.FileSystemGlobbing; using Microsoft.Extensions.Primitives; -namespace Microsoft.AspNetCore.Hosting.StaticWebAssets +namespace Microsoft.AspNetCore.StaticWebAssets { - // A file provider used for serving static web assets from referenced projects and packages during development. - // The file provider maps folders from referenced projects and packages and prepends a prefix to their relative - // paths. - // At publish time the assets end up in the wwwroot folder of the published app under the prefix indicated here - // as the base path. - // For example, for a referenced project mylibrary with content under <<mylibrarypath>>\wwwroot will expose - // static web assets under _content/mylibrary (this is by convention). The path prefix or base path we apply - // is that (_content/mylibrary). - // when the app gets published, the build pipeline puts the static web assets for mylibrary under - // publish/wwwroot/_content/mylibrary/sample-asset.js - // To allow for the same experience during development, StaticWebAssetsFileProvider maps the contents of - // <<mylibrarypath>>\wwwroot\** to _content/mylibrary/** - internal class StaticWebAssetsFileProvider : IFileProvider - { - private static readonly StringComparison FilePathComparison = OperatingSystem.IsWindows() ? - StringComparison.OrdinalIgnoreCase : - StringComparison.Ordinal; - - public StaticWebAssetsFileProvider(string pathPrefix, string contentRoot) - { - BasePath = NormalizePath(pathPrefix); - InnerProvider = new PhysicalFileProvider(contentRoot); - } - - public PhysicalFileProvider InnerProvider { get; } - - public PathString BasePath { get; } - - /// <inheritdoc /> - public IDirectoryContents GetDirectoryContents(string subpath) - { - var modifiedSub = NormalizePath(subpath); - - if (BasePath == "/") - { - return InnerProvider.GetDirectoryContents(modifiedSub); - } - - if (StartsWithBasePath(modifiedSub, out var physicalPath)) - { - return InnerProvider.GetDirectoryContents(physicalPath.Value); - } - else if (string.Equals(subpath, string.Empty) || string.Equals(modifiedSub, "/")) - { - return new StaticWebAssetsDirectoryRoot(BasePath); - } - else if (BasePath.StartsWithSegments(modifiedSub, FilePathComparison, out var remaining)) - { - return new StaticWebAssetsDirectoryRoot(remaining); - } - - return NotFoundDirectoryContents.Singleton; - } - - /// <inheritdoc /> - public IFileInfo GetFileInfo(string subpath) - { - var modifiedSub = NormalizePath(subpath); - - if (BasePath == "/") - { - return InnerProvider.GetFileInfo(subpath); - } - - if (!StartsWithBasePath(modifiedSub, out var physicalPath)) - { - return new NotFoundFileInfo(subpath); - } - else - { - return InnerProvider.GetFileInfo(physicalPath.Value); - } - } - - /// <inheritdoc /> - public IChangeToken Watch(string filter) - { - return InnerProvider.Watch(filter); - } - - private static string NormalizePath(string path) - { - path = path.Replace('\\', '/'); - return path.StartsWith('/') ? path : "/" + path; - } - - private bool StartsWithBasePath(string subpath, out PathString rest) - { - return new PathString(subpath).StartsWithSegments(BasePath, FilePathComparison, out rest); - } - - private class StaticWebAssetsDirectoryRoot : IDirectoryContents - { - private readonly string _nextSegment; - - public StaticWebAssetsDirectoryRoot(PathString remainingPath) - { - // We MUST use the Value property here because it is unescaped. - _nextSegment = remainingPath.Value?.Split("/", StringSplitOptions.RemoveEmptyEntries).FirstOrDefault() ?? string.Empty; - } - - public bool Exists => true; - - public IEnumerator<IFileInfo> GetEnumerator() - { - return GenerateEnum(); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return GenerateEnum(); - } - - private IEnumerator<IFileInfo> GenerateEnum() - { - return new[] { new StaticWebAssetsFileInfo(_nextSegment) } - .Cast<IFileInfo>().GetEnumerator(); - } - - private class StaticWebAssetsFileInfo : IFileInfo - { - public StaticWebAssetsFileInfo(string name) - { - Name = name; - } - - public bool Exists => true; - - public long Length => throw new NotImplementedException(); - - public string PhysicalPath => throw new NotImplementedException(); - - public DateTimeOffset LastModified => throw new NotImplementedException(); - - public bool IsDirectory => true; - - public string Name { get; } - - public Stream CreateReadStream() - { - throw new NotImplementedException(); - } - } - } - } - internal sealed class ManifestStaticWebAssetFileProvider : IFileProvider { private static readonly StringComparison _fsComparison = OperatingSystem.IsWindows() ? -- GitLab