Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,7 @@ Copyright (c) .NET Foundation. All rights reserved.
SourceType="Computed"
AssetKind="Build"
AssetRole="Primary"
CopyToOutputDirectory="PreserveNewest"
CopyToOutputDirectory="Never"
CopyToPublishDirectory="Never"
FingerprintCandidates="$(_WasmFingerprintAssets)"
FingerprintPatterns="@(FingerprintPatterns);@(_WasmFingerprintPatterns)"
Expand All @@ -406,7 +406,7 @@ Copyright (c) .NET Foundation. All rights reserved.
SourceType="Framework"
AssetKind="Build"
AssetRole="Primary"
CopyToOutputDirectory="PreserveNewest"
CopyToOutputDirectory="Never"
CopyToPublishDirectory="Never"
FingerprintCandidates="$(_WasmFingerprintAssets)"
FingerprintPatterns="@(FingerprintPatterns);@(_WasmFingerprintPatterns)"
Expand All @@ -430,12 +430,12 @@ Copyright (c) .NET Foundation. All rights reserved.
<!-- Materialized framework assets must be visible to referencing projects (e.g. Blazor WASM
hosted scenarios where the server project serves the client's framework files).
UpdatePackageStaticWebAssets defaults AssetMode to CurrentProject and CopyToOutputDirectory
to Never. Override both: AssetMode=All so assets flow through project references, and
CopyToOutputDirectory=PreserveNewest so they are copied from the intermediate materialized
path (obj/fx/{SourceId}/) to bin/wwwroot/_framework/ at build time. -->
to Never. Override AssetMode=All so assets flow through project references. Framework
files stay in obj/fx/{SourceId}/ and are served by the static web assets middleware
during dotnet run — they are not copied to bin/wwwroot/_framework/ at build time. -->
<ItemGroup>
<_WasmMaterializedFrameworkAssets Update="@(_WasmMaterializedFrameworkAssets)"
AssetMode="All" CopyToOutputDirectory="PreserveNewest" />
AssetMode="All" CopyToOutputDirectory="Never" />
<WasmStaticWebAsset Include="@(_WasmMaterializedFrameworkAssets)" />
Comment thread
maraf marked this conversation as resolved.
</ItemGroup>

Expand Down
16 changes: 14 additions & 2 deletions src/mono/wasm/Wasm.Build.Tests/Blazor/BuildPublishTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -95,14 +95,26 @@ public void DefaultTemplate_WithResources_Publish(Configuration config)

Utils.DirectoryCopy(resxSourcePath, Path.Combine(_projectDir, "resx"));

// Build and assert resource dlls
// Build and assert resource dlls. With CopyToOutputDirectory=Never, framework files
// (including satellite assemblies) are no longer copied to bin/_framework/ during build.
// Webcil-converted assemblies live under obj/{config}/{tfm}/webcil/{locale}/; when webcil
// is disabled they're materialized under obj/{config}/{tfm}/fx/{name}/_framework/{locale}/.
BlazorBuild(info, config);
AssertResourcesDlls(GetBlazorBinFrameworkDir(config, forPublish: false));
AssertResourcesDlls(GetBuildSatelliteBaseDir());

// Publish and assert resource dlls
BlazorPublish(info, config, new PublishOptions(UseCache: false));
AssertResourcesDlls(GetBlazorBinFrameworkDir(config, forPublish: true));

string GetBuildSatelliteBaseDir()
{
string objDir = Path.Combine(_projectDir, "obj", config.ToString(), DefaultTargetFrameworkForBlazor);
if (UseWebcil)
return Path.Combine(objDir, "webcil");

return WasmSdkBasedProjectProvider.GetMaterializedFrameworkDir(objDir);
}

void AssertResourcesDlls(string basePath)
{
foreach (string culture in cultures)
Expand Down
49 changes: 43 additions & 6 deletions src/mono/wasm/Wasm.Build.Tests/Blazor/MiscTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.NET.Sdk.WebAssembly;
using Microsoft.Playwright;
using Xunit;
using Xunit.Abstractions;
using Xunit.Sdk;
Expand Down Expand Up @@ -107,7 +108,7 @@ public void BugRegression_60479_WithRazorClassLib()
[InlineData(Configuration.Release, false)]
[InlineData(Configuration.Debug, true)]
[InlineData(Configuration.Release, true)]
public void MultiClientHostedBuildAndPublish(Configuration config, bool publish)
public async Task MultiClientHostedBuildAndPublish(Configuration config, bool publish)
{
// Test that two Blazor WASM client projects can be built/published by a single server
// project without duplicate static web asset Identity collisions. This validates the
Expand Down Expand Up @@ -148,16 +149,52 @@ public void MultiClientHostedBuildAndPublish(Configuration config, bool publish)
}
else
{
string client1Framework = Path.Combine(client1Dir, "bin", config.ToString(), DefaultTargetFrameworkForBlazor, "wwwroot", "_framework");
string client2Framework = Path.Combine(client2Dir, "bin", config.ToString(), DefaultTargetFrameworkForBlazor, "wwwroot", "_framework");

Assert.True(Directory.Exists(client1Framework), $"Client1 framework dir missing: {client1Framework}");
Assert.True(Directory.Exists(client2Framework), $"Client2 framework dir missing: {client2Framework}");
// With CopyToOutputDirectory=Never, framework files are no longer copied to
// bin/_framework/ during build. UpdatePackageStaticWebAssets materializes them
// under obj/<config>/<tfm>/fx/<source-id>/_framework/ instead (the source-id
// folder name comes from static web assets metadata and isn't necessarily the
// project directory name), and the static web assets middleware serves them
// from there during dotnet run.
string client1ObjDir = Path.Combine(client1Dir, "obj", config.ToString(), DefaultTargetFrameworkForBlazor);
string client2ObjDir = Path.Combine(client2Dir, "obj", config.ToString(), DefaultTargetFrameworkForBlazor);
string client1Framework = WasmSdkBasedProjectProvider.GetMaterializedFrameworkDir(client1ObjDir);
string client2Framework = WasmSdkBasedProjectProvider.GetMaterializedFrameworkDir(client2ObjDir);

var client1Files = Directory.GetFiles(client1Framework);
var client2Files = Directory.GetFiles(client2Framework);
Assert.Contains(client1Files, f => Path.GetFileName(f).StartsWith("dotnet.") && f.EndsWith(".js"));
Assert.Contains(client2Files, f => Path.GetFileName(f).StartsWith("dotnet.") && f.EndsWith(".js"));
Assert.Contains(client1Files, f => Path.GetFileName(f).Contains("dotnet.native") && f.EndsWith(".wasm"));
Assert.Contains(client2Files, f => Path.GetFileName(f).Contains("dotnet.native") && f.EndsWith(".wasm"));

// Start the server via `dotnet run --no-build` and verify that the static web assets
// middleware serves each referenced client's framework files from its obj/ materialized
// directory. This guards the hosted scenario the inline comment in
// Microsoft.NET.Sdk.WebAssembly.Browser.targets calls out: a server project consuming
// client framework files after `dotnet build` only (no publish). The Development
// environment is required so the Web SDK auto-invokes UseStaticWebAssets() and loads
// the client projects' static web assets manifests.
using RunCommand runCmd = new RunCommand(s_buildEnv, _testOutput);
runCmd.WithEnvironmentVariable("ASPNETCORE_ENVIRONMENT", "Development");
ToolCommand serverCmd = runCmd.WithWorkingDirectory(serverDir);
await using BrowserRunner runner = new BrowserRunner(_testOutput);
string serverUrl = await runner.StartServerAndGetUrlAsync(
serverCmd, $"run -c {config} --no-build");
// Kestrel logs the listening URL as http://[::]:<port> (wildcard) which is not a valid
// Host header — replace the host with localhost for client requests.
string clientBaseUrl = serverUrl.Replace("[::]", "localhost");

IBrowser browser = await runner.SpawnBrowserAsync(serverUrl);
IBrowserContext context = await browser.NewContextAsync(
new() { BaseURL = clientBaseUrl, IgnoreHTTPSErrors = true });
foreach (string clientBase in new[] { "client1", "client2" })
{
string assetUrl = $"/{clientBase}/_framework/dotnet.js";
_testOutput.WriteLine($"Fetching {clientBaseUrl}{assetUrl} via Playwright");
var response = await context.APIRequest.GetAsync(assetUrl);
Assert.True(response.Ok,
$"Expected 2xx for {clientBaseUrl}{assetUrl} but got {response.Status} {response.StatusText}");
}
}
}
}
29 changes: 28 additions & 1 deletion src/mono/wasm/Wasm.Build.Tests/BuildPublishTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,35 @@ public async Task BuildThenPublishWithAOT(Configuration config, bool aot)
(_, string output) = BuildProject(info, config, new BuildOptions(Label: "first_build", AOT: aot), isNativeBuild: aot);

BuildPaths paths = GetBuildPaths(config, forPublish: isPublish);
// With CopyToOutputDirectory=Never, framework files aren't copied to bin/_framework/
// during build. They live in obj subdirs: dotnet.native.* in obj/wasm/for-build/
// (for native rebuilds like Release+AOT), and JS/source maps in obj/{config}/{tfm}/fx/{source-id}/_framework/.
// The boot config (dotnet.js) is at obj/{config}/{tfm}/dotnet.js.
string fxFrameworkDir = WasmSdkBasedProjectProvider.GetMaterializedFrameworkDir(paths.ObjDir);

BuildPaths buildObjPaths = paths with { BinFrameworkDir = fxFrameworkDir };
IDictionary<string, (string fullPath, bool unchanged)> pathsDict =
GetFilesTable(info.ProjectName, aot, paths, unchanged: false);
GetFilesTable(info.ProjectName, aot, buildObjPaths, unchanged: false, bootConfigDir: paths.ObjDir);

// dotnet.native.* are produced by the native rebuild into obj/wasm/for-build/ using
// their canonical (non-fingerprinted) names — fingerprinting is applied later when
// publishing to bin.
foreach (var nativeName in new[] { "dotnet.native.wasm", "dotnet.native.js" })
{
if (pathsDict.TryGetValue(nativeName, out var entry))
{
pathsDict[nativeName] = (Path.Combine(paths.ObjWasmDir, nativeName), entry.unchanged);
}
}

// dotnet.runtime.js lives in the materialized fx dir under its canonical (non-fingerprinted)
// name during build — fingerprinting is applied later when publishing to bin. GetFilesTable
// rewrites this entry to the fingerprinted name (from the boot config, which already reflects
// the publish layout), so override it back to the unfingerprinted obj/fx path.
if (pathsDict.TryGetValue("dotnet.runtime.js", out var runtimeJsEntry))
{
pathsDict["dotnet.runtime.js"] = (Path.Combine(fxFrameworkDir, "dotnet.runtime.js"), runtimeJsEntry.unchanged);
}

string mainDll = $"{info.ProjectName}.dll";
var firstBuildStat = StatFiles(pathsDict);
Expand Down
32 changes: 27 additions & 5 deletions src/mono/wasm/Wasm.Build.Tests/ModuleConfigTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -120,11 +120,33 @@ private void SymbolMapFileEmittedCore(bool emitSymbolMap, bool isPublish)
else
BuildProject(info, config, new BuildOptions(AssertAppBundle: false));

string frameworkDir = GetBinFrameworkDir(config, forPublish: isPublish);

// The file may be fingerprinted (e.g. dotnet.native.<hash>.js.symbols),
// so use a glob pattern to find it.
bool symbolsFileExists = Directory.EnumerateFiles(frameworkDir, "dotnet.native*.js.symbols").Any();
// Locate the emitted symbols file. With CopyToOutputDirectory=Never, framework files are
// no longer in bin/_framework during build: the native symbols file lives in
// obj/{config}/{tfm}/wasm/for-build/ (native rebuild) and the materialized copy ends up
// in obj/{config}/{tfm}/fx/{name}/_framework/. The publish path still has them in
// bin/{config}/{tfm}/publish/wwwroot/_framework/.
// The file may be fingerprinted (e.g. dotnet.native.<hash>.js.symbols), so use a glob.
const string symbolsPattern = "dotnet.native*.js.symbols";
bool symbolsFileExists;
if (isPublish)
{
string frameworkDir = GetBinFrameworkDir(config, forPublish: true);
symbolsFileExists = Directory.EnumerateFiles(frameworkDir, symbolsPattern).Any();
}
else
{
string objDir = Path.Combine(_projectDir, "obj", config.ToString(), DefaultTargetFramework);
string fxBaseDir = Path.Combine(objDir, "fx");
string[] searchDirs = [
Path.Combine(objDir, "wasm", "for-build"),
.. Directory.Exists(fxBaseDir)
? Directory.GetDirectories(fxBaseDir).Select(d => Path.Combine(d, "_framework"))
: Array.Empty<string>()
];
symbolsFileExists = searchDirs
.Where(Directory.Exists)
.Any(d => Directory.EnumerateFiles(d, symbolsPattern).Any());
}
Assert.Equal(emitSymbolMap, symbolsFileExists);
}
}
4 changes: 2 additions & 2 deletions src/mono/wasm/Wasm.Build.Tests/ProjectProviderBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,7 @@ private string[] GetFilesMatchingNameConsideringFingerprinting(string filePath,
return dict;
}

public IDictionary<string, (string fullPath, bool unchanged)> GetFilesTable(string projectName, bool isAOT, BuildPaths paths, bool unchanged)
public IDictionary<string, (string fullPath, bool unchanged)> GetFilesTable(string projectName, bool isAOT, BuildPaths paths, bool unchanged, string? bootConfigDir = null)
{
List<string> files = new()
{
Expand Down Expand Up @@ -379,7 +379,7 @@ private string[] GetFilesMatchingNameConsideringFingerprinting(string filePath,

if (IsFingerprintingEnabled)
{
string bootJsonPath = GetBootConfigPath(paths.BinFrameworkDir, "dotnet.js");
string bootJsonPath = GetBootConfigPath(bootConfigDir ?? paths.BinFrameworkDir, "dotnet.js");
BootJsonData bootJson = GetBootJson(bootJsonPath);
AssetsData assets = (AssetsData)bootJson.resources;
var keysToUpdate = new List<string>();
Expand Down
13 changes: 9 additions & 4 deletions src/mono/wasm/Wasm.Build.Tests/SatelliteLoadingTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -110,15 +110,20 @@ public void SatelliteAssembliesFromPackageReference()
ProjectInfo info = CopyTestAsset(config, false, TestAsset.WasmBasicTestApp, "SatelliteLoadingTestsFromPackageReference");
BuildProject(info, config, new BuildOptions(ExtraMSBuildArgs: "-p:TestSatelliteAssembliesFromPackage=true"));

string binFrameworkDir = GetBinFrameworkDir(config, forPublish: false);
// With CopyToOutputDirectory=Never, satellite assemblies are no longer in
// bin/_framework/. When webcil is enabled they're in obj/{config}/{tfm}/webcil/{locale}/;
// when disabled they're materialized in obj/{config}/{tfm}/fx/{name}/_framework/{locale}/.
string objDir = Path.Combine(_projectDir, "obj", config.ToString(), DefaultTargetFramework);
string satelliteBaseDir = UseWebcil
? Path.Combine(objDir, "webcil")
: WasmSdkBasedProjectProvider.GetMaterializedFrameworkDir(objDir);

// Microsoft.CodeAnalysis.CSharp has satellite assemblies for multiple locales
// Verify that at least some of them are present in the AppBundle
string[] expectedLocales = ["cs", "de", "es", "fr", "it", "ja", "ko", "pl", "pt-BR", "ru", "tr", "zh-Hans", "zh-Hant"];
foreach (string locale in expectedLocales)
{
string satelliteDir = Path.Combine(binFrameworkDir, locale);
Assert.True(Directory.Exists(satelliteDir), $"Expected satellite directory '{locale}' to exist in {binFrameworkDir}");
string satelliteDir = Path.Combine(satelliteBaseDir, locale);
Assert.True(Directory.Exists(satelliteDir), $"Expected satellite directory '{locale}' to exist in {satelliteBaseDir}");

string[] satelliteFiles = Directory.GetFiles(satelliteDir, "Microsoft.CodeAnalysis.CSharp.resources*");
Assert.True(satelliteFiles.Length > 0, $"Expected Microsoft.CodeAnalysis.CSharp.resources.dll in {satelliteDir}");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -497,8 +497,8 @@ public string GetObjDir(Configuration config, string? framework = null, string?
public BuildPaths GetBuildPaths(Configuration config, bool forPublish, string? projectDir = null) =>
_provider.GetBuildPaths(config, forPublish, projectDir);

public IDictionary<string, (string fullPath, bool unchanged)> GetFilesTable(string projectName, bool isAOT, BuildPaths paths, bool unchanged) =>
_provider.GetFilesTable(projectName, isAOT, paths, unchanged);
public IDictionary<string, (string fullPath, bool unchanged)> GetFilesTable(string projectName, bool isAOT, BuildPaths paths, bool unchanged, string? bootConfigDir = null) =>
_provider.GetFilesTable(projectName, isAOT, paths, unchanged, bootConfigDir);

public IDictionary<string, FileStat> StatFiles(IDictionary<string, (string fullPath, bool unchanged)> fullpaths) =>
_provider.StatFiles(fullpaths);
Expand Down
Loading
Loading