diff --git a/src/mono/wasm/Wasm.Build.Tests/Blazor/AppsettingsTests.cs b/src/mono/wasm/Wasm.Build.Tests/Blazor/AppsettingsTests.cs new file mode 100644 index 00000000000000..89ecb5b3c6a32a --- /dev/null +++ b/src/mono/wasm/Wasm.Build.Tests/Blazor/AppsettingsTests.cs @@ -0,0 +1,57 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +#nullable enable + +namespace Wasm.Build.Tests.Blazor; + +public class AppsettingsTests : BuildTestBase +{ + public AppsettingsTests(ITestOutputHelper output, SharedBuildPerTestClassFixture buildContext) + : base(output, buildContext) + { + _enablePerTestCleanup = true; + } + + [Fact] + public async Task FileInVfs() + { + string id = $"blazor_{Path.GetRandomFileName()}"; + string projectFile = CreateWasmTemplateProject(id, "blazorwasm"); + + string projectDirectory = Path.GetDirectoryName(projectFile)!; + + File.WriteAllText(Path.Combine(projectDirectory, "wwwroot", "appsettings.json"), $"{{ \"Id\": \"{id}\" }}"); + + string programPath = Path.Combine(projectDirectory, "Program.cs"); + string programContent = File.ReadAllText(programPath); + programContent = programContent.Replace("var builder", + """ + System.Console.WriteLine($"appSettings Exists '{File.Exists("/appsettings.json")}'"); + System.Console.WriteLine($"appSettings Content '{File.ReadAllText("/appsettings.json")}'"); + var builder + """); + File.WriteAllText(programPath, programContent); + + BlazorBuild(new BlazorBuildOptions(id, "debug", NativeFilesType.FromRuntimePack)); + + bool existsChecked = false; + bool contentChecked = false; + + await BlazorRunForBuildWithDotnetRun("debug", onConsoleMessage: msg => + { + if (msg.Text.Contains("appSettings Exists 'True'")) + existsChecked = true; + else if (msg.Text.Contains($"appSettings Content '{{ \"Id\": \"{id}\" }}'")) + contentChecked = true; + }); + + Assert.True(existsChecked, "File '/appsettings.json' wasn't found"); + Assert.True(contentChecked, "Content of '/appsettings.json' is not matched"); + } +} \ No newline at end of file diff --git a/src/mono/wasm/Wasm.Build.Tests/BuildTestBase.cs b/src/mono/wasm/Wasm.Build.Tests/BuildTestBase.cs index 17bc1d17ae79f9..8a0a8d3980d7f5 100644 --- a/src/mono/wasm/Wasm.Build.Tests/BuildTestBase.cs +++ b/src/mono/wasm/Wasm.Build.Tests/BuildTestBase.cs @@ -837,7 +837,7 @@ protected void AssertBlazorBundle(string config, bool isPublish, bool dotnetWasm same: dotnetWasmFromRuntimePack); } - protected void AssertBlazorBootJson(string config, bool isPublish, bool isNet7AndBelow, string targetFramework = DefaultTargetFrameworkForBlazor, string? binFrameworkDir=null) + protected void AssertBlazorBootJson(string config, bool isPublish, bool isNet7AndBelow, string targetFramework = DefaultTargetFrameworkForBlazor, string? binFrameworkDir = null) { binFrameworkDir ??= FindBlazorBinFrameworkDir(config, isPublish, targetFramework); @@ -932,17 +932,18 @@ public void BlazorAddRazorButton(string buttonText, string customCode, string me // Keeping these methods with explicit Build/Publish in the name // so in the test code it is evident which is being run! - public Task BlazorRunForBuildWithDotnetRun(string config, Func? test = null, string extraArgs = "--no-build") - => BlazorRunTest($"run -c {config} {extraArgs}", _projectDir!, test); + public Task BlazorRunForBuildWithDotnetRun(string config, Func? test = null, string extraArgs = "--no-build", Action? onConsoleMessage = null) + => BlazorRunTest($"run -c {config} {extraArgs}", _projectDir!, test, onConsoleMessage); - public Task BlazorRunForPublishWithWebServer(string config, Func? test = null, string extraArgs = "") + public Task BlazorRunForPublishWithWebServer(string config, Func? test = null, string extraArgs = "", Action? onConsoleMessage = null) => BlazorRunTest($"{s_xharnessRunnerCommand} wasm webserver --app=. --web-server-use-default-files {extraArgs}", Path.GetFullPath(Path.Combine(FindBlazorBinFrameworkDir(config, forPublish: true), "..")), - test); + test, onConsoleMessage); public async Task BlazorRunTest(string runArgs, string workingDirectory, Func? test = null, + Action? onConsoleMessage = null, bool detectRuntimeFailures = true) { using var runCommand = new RunCommand(s_buildEnv, _testOutput) @@ -968,6 +969,8 @@ void OnConsoleMessage(IConsoleMessage msg) Console.WriteLine($"[{msg.Type}] {msg.Text}"); _testOutput.WriteLine($"[{msg.Type}] {msg.Text}"); + onConsoleMessage?.Invoke(msg); + if (detectRuntimeFailures) { if (msg.Text.Contains("[MONO] * Assertion") || msg.Text.Contains("Error: [MONO] ")) diff --git a/src/mono/wasm/runtime/loader/blazor/BootConfig.ts b/src/mono/wasm/runtime/loader/blazor/BootConfig.ts index 9df0f5776937b1..2dd751cbf748e4 100644 --- a/src/mono/wasm/runtime/loader/blazor/BootConfig.ts +++ b/src/mono/wasm/runtime/loader/blazor/BootConfig.ts @@ -5,7 +5,7 @@ import type { BootJsonData } from "../../types/blazor"; import type { WebAssemblyBootResourceType } from "../../types"; import { loaderHelpers } from "../globals"; -type LoadBootResourceCallback = (type: WebAssemblyBootResourceType, name: string, defaultUri: string, integrity: string) => string | Promise | null | undefined; +export type LoadBootResourceCallback = (type: WebAssemblyBootResourceType, name: string, defaultUri: string, integrity: string) => string | Promise | null | undefined; export class BootConfigResult { private constructor(public bootConfig: BootJsonData, public applicationEnvironment: string) { diff --git a/src/mono/wasm/runtime/loader/blazor/WebAssemblyResourceLoader.ts b/src/mono/wasm/runtime/loader/blazor/WebAssemblyResourceLoader.ts index 8af4dba109e0d6..d0960a287e96ab 100644 --- a/src/mono/wasm/runtime/loader/blazor/WebAssemblyResourceLoader.ts +++ b/src/mono/wasm/runtime/loader/blazor/WebAssemblyResourceLoader.ts @@ -6,6 +6,8 @@ import type { BootJsonData, ResourceList } from "../../types/blazor"; import { toAbsoluteUri } from "./_Polyfill"; const networkFetchCacheMode = "no-cache"; +const cacheSkipResourceTypes = ["configuration"]; + export class WebAssemblyResourceLoader { private usedCacheKeys: { [key: string]: boolean } = {}; @@ -27,7 +29,7 @@ export class WebAssemblyResourceLoader { } loadResource(name: string, url: string, contentHash: string, resourceType: WebAssemblyBootResourceType): LoadingResource { - const response = this.cacheIfUsed + const response = this.cacheIfUsed && !cacheSkipResourceTypes.includes(resourceType) ? this.loadResourceWithCaching(this.cacheIfUsed, name, url, contentHash, resourceType) : this.loadResourceWithoutCaching(name, url, contentHash, resourceType); @@ -127,10 +129,19 @@ export class WebAssemblyResourceLoader { // Note that if cacheBootResources was explicitly disabled, we also bypass hash checking // This is to give developers an easy opt-out from the entire caching/validation flow if // there's anything they don't like about it. - return fetch(url, { - cache: networkFetchCacheMode, - integrity: this.bootConfig.cacheBootResources ? contentHash : undefined, - }); + const fetchOptions: RequestInit = { + cache: networkFetchCacheMode + }; + + if (resourceType === "configuration") { + // Include credentials so the server can allow download / provide user specific file + fetchOptions.credentials = "include"; + } else { + // Any other resource than configuration should provide integrity check + fetchOptions.integrity = this.bootConfig.cacheBootResources ? contentHash : undefined; + } + + return fetch(url, fetchOptions); } private async addToCacheAsync(cache: Cache, name: string, cacheKey: string, response: Response) { diff --git a/src/mono/wasm/runtime/loader/blazor/_Integration.ts b/src/mono/wasm/runtime/loader/blazor/_Integration.ts index 393658b67668a7..cfb34fe53fa50c 100644 --- a/src/mono/wasm/runtime/loader/blazor/_Integration.ts +++ b/src/mono/wasm/runtime/loader/blazor/_Integration.ts @@ -22,7 +22,7 @@ export async function loadBootConfig(config: MonoConfigInternal, module: DotnetM } export async function initializeBootConfig(bootConfigResult: BootConfigResult, module: DotnetModuleInternal, startupOptions?: Partial) { - INTERNAL.resourceLoader = resourceLoader = await WebAssemblyResourceLoader.initAsync(bootConfigResult.bootConfig, startupOptions || {}); + INTERNAL.resourceLoader = resourceLoader = await WebAssemblyResourceLoader.initAsync(bootConfigResult.bootConfig, startupOptions ?? {}); mapBootConfigToMonoConfig(loaderHelpers.config, bootConfigResult.applicationEnvironment); setupModuleForBlazor(module); } @@ -31,21 +31,20 @@ let resourcesLoaded = 0; let totalResources = 0; const behaviorByName = (name: string): AssetBehaviours | "other" => { - return name === "dotnet.timezones.blat" ? "vfs" - : name === "dotnet.native.wasm" ? "dotnetwasm" - : (name.startsWith("dotnet.native.worker") && name.endsWith(".js")) ? "js-module-threads" - : (name.startsWith("dotnet.native") && name.endsWith(".js")) ? "js-module-native" - : (name.startsWith("dotnet.runtime") && name.endsWith(".js")) ? "js-module-runtime" - : (name.startsWith("dotnet") && name.endsWith(".js")) ? "js-module-dotnet" - : name.startsWith("icudt") ? "icu" - : "other"; + return name === "dotnet.native.wasm" ? "dotnetwasm" + : (name.startsWith("dotnet.native.worker") && name.endsWith(".js")) ? "js-module-threads" + : (name.startsWith("dotnet.native") && name.endsWith(".js")) ? "js-module-native" + : (name.startsWith("dotnet.runtime") && name.endsWith(".js")) ? "js-module-runtime" + : (name.startsWith("dotnet") && name.endsWith(".js")) ? "js-module-dotnet" + : name.startsWith("icudt") ? "icu" + : "other"; }; const monoToBlazorAssetTypeMap: { [key: string]: WebAssemblyBootResourceType | undefined } = { "assembly": "assembly", "pdb": "pdb", "icu": "globalization", - "vfs": "globalization", + "vfs": "configuration", "dotnetwasm": "dotnetwasm", }; @@ -161,6 +160,16 @@ export function mapBootConfigToMonoConfig(moduleConfig: MonoConfigInternal, appl }; assets.push(asset); } + for (let i = 0; i < resourceLoader.bootConfig.config.length; i++) { + const config = resourceLoader.bootConfig.config[i]; + if (config === "appsettings.json" || config === `appsettings.${applicationEnvironment}.json`) { + assets.push({ + name: config, + resolvedUrl: config, + behavior: "vfs", + }); + } + } if (!hasIcuData) { moduleConfig.globalizationMode = "invariant"; diff --git a/src/mono/wasm/runtime/types/blazor.ts b/src/mono/wasm/runtime/types/blazor.ts index 95f88fd1017754..bf3ec8da02aa04 100644 --- a/src/mono/wasm/runtime/types/blazor.ts +++ b/src/mono/wasm/runtime/types/blazor.ts @@ -45,4 +45,4 @@ export enum ICUDataMode { All = 1, Invariant = 2, Custom = 3 -} +} \ No newline at end of file