diff --git a/src/Components/Authorization/src/AuthorizeRouteView.cs b/src/Components/Authorization/src/AuthorizeRouteView.cs index ef1fb45dfd9c5cd50475fd43978600b927192ada..291da9bf12d859634e58fe5625148053ef0d4fc9 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 66c8c260e8c87603ad6f1e356809dc302f34b4b4..e8f40cb25a75d793d815165dd74aea33c6590ccc 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 2b021d4aa08b8a87657d19975918834530f06ab5..aebe39281f0403fd370ed1db88459e0f19e6d004 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 0000000000000000000000000000000000000000..845f65308eacd12253bbc1567b0013cc618c9088 --- /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 1d6323e53f382ca530bd747413922992f1ad5800..2eb51b174f4a49d4a60c57d065c6980877a98bb0 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 0000000000000000000000000000000000000000..e1e70427b9a6bee491056e50ed855e9fbf3c5409 --- /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 bb802199a7abca9a9e98d5ed2660179bc7329f1f..256b3f2e787ae062ae8a55e5821d2bd6dae92400 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 3a7cd4ce842626cb86b71cb3cabab9a14465079b..43bd3c85c997696b8959cc43c496d9853c60ec8f 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 7a6e892fc50d6bbc93ec837d8bce7a9c9c1695d9..977228551a24ad69d5a369624c96470771edbcc9 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 Binary files a/src/Components/Web.JS/dist/Release/blazor.server.js and b/src/Components/Web.JS/dist/Release/blazor.server.js differ 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 Binary files a/src/Components/Web.JS/dist/Release/blazor.webview.js and b/src/Components/Web.JS/dist/Release/blazor.webview.js differ diff --git a/src/Components/Web.JS/src/Boot.Server.ts b/src/Components/Web.JS/src/Boot.Server.ts index 5e23268a67dac06c1d3206707b535874acd1257f..9f5fc9e608b402137e36f8cb161207f657808582 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 13b6f2b44322a9d0d137e5576877700cfc7eba04..76e103a1f12d36d70b03fbc04d199db7e99c971c 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 0b5a5f17767cfe3df6e0f62ee2a949e5191da22f..b7740928550604255c84a928cc205f71d23f2c17 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 0000000000000000000000000000000000000000..44a9416901462dc5007da0d69cf85c2d31d6fde1 --- /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 0000000000000000000000000000000000000000..23127e0b6621f3437a49092f6c630625baac7b6f --- /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 0000000000000000000000000000000000000000..1cfd05092c48af7e64d6f2c453282ee36f52a2ec --- /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 0000000000000000000000000000000000000000..da0634357ffc2046f97428beb6c998f2c56399df --- /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 6a0079e5f16493a93724c342d29dd3ee070c1f79..55ad90380d0857f128c2c545b24412b793e364ae 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 ffacdbb11f4d122ec95f3d9b7c30ab4e733101e9..c48a5100a5d3fe7619eed98468f11beac7313025 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 a7813ded5916e6ad8bf2d7ef9c41c6a2768463b4..e1595b625dff3e28decd73100a4544c12d5fa480 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 c37466ceac28e846cdd3e6634b8b42b6f6f7f929..1418ea5ab4319cb6017adbc4a4c92acc4540af10 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 c8fc38f0add3c4798331b4ca5fd56b2b13efd393..01db2752e5c217462fb78cbaea07359b76d8d359 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 6ef6d4adc3985c8476a5ac18fa2dc81844e319c9..4b10fb01378b5f985b49c4aa2aaaa85ff7e8a12f 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 db2817a9542341df422e5ca5d090b9a642938593..fcf1c19a2f955f5daf8ef29f55af31ab5d6b27d5 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 b9bdce6a5ea56932c00b5b45d50991193e527a84..2e581d2a73f9ecc265b9a06496f77083420a08ad 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 075ed8d99f99c37193d432e6ca9e6d8d960b02a3..89f647db6b443ce15342d5c9cc78875ed22cb09e 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 f1202ba0a9b66bb0e21989c1b30d9a367f6f2b4f..0000000000000000000000000000000000000000 --- 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 885473032c266660c9086ba0ad3e9b27569f2210..cc285129f61fa9e0f05da59e870d22cd23b2f859 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 0000000000000000000000000000000000000000..fe51488c7066f6687ef680d6bfaa4f7768ef205c --- /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 0000000000000000000000000000000000000000..f43c2047fb3169b866d2179f1ebc52b56a5b0613 --- /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 0000000000000000000000000000000000000000..d39e70a2d6a57bd42579d5a832f17ec60074f4f1 --- /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 0000000000000000000000000000000000000000..e8fc13715967a4ac277e90d347378b33fd0b9ff8 --- /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 0000000000000000000000000000000000000000..d0e8652a3c489cea0a433a934b0e95132186449f --- /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 22c9c43e82936a55262a294f285c19082dec68b0..553de9db70067041d893baa8ef4879744e10dd39 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 0000000000000000000000000000000000000000..965a355da10969859eebf3ee8248b76cf429e22a --- /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 d647825937427b7ccc565f506b171b54c508a241..107e1d40c68a43a1409038ea086313c1366da9a5 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 483fff518cfeea7edb89c79a48e34d946a4fb80f..f353900b93e07e1643253a959908b225cdd4c499 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 6f1bae0b68247995cc23e588d23a08c73f9b680f..0000000000000000000000000000000000000000 --- 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 198ce8932f09c15e5d8ea209f6806f271e6b7567..24f537afc6e6bfd7f3c4a5fe004341e8b022b3ef 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 fa13a43c497935f351861001174ccde280ebc950..0000000000000000000000000000000000000000 --- 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 f714b34407165ef0b4d1165425bf810746d867b8..0000000000000000000000000000000000000000 --- 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 912e817dca9d97debd4ebe4ae4c9d500ab844a60..de41e17e4ad3e5013247630c5b2471438b05d270 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 fd8b8cffa5890c6f878ccf07df5fac93d3c60f28..a5a5ed15598bc33f94662700ae5911e4e24bbbc6 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() ?