Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions eng/testing/scenarios/BuildWasmAppsJobsList.txt
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,4 @@ Wasm.Build.Tests.PreloadingTests
Wasm.Build.Tests.EnvVariablesTests
Wasm.Build.Tests.HttpTests
Wasm.Build.Tests.DiagnosticsTests
Wasm.Build.Tests.FilesToIncludeInFileSystemTests
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ Copyright (c) .NET Foundation. All rights reserved.
</PropertyGroup>

<ItemGroup>
<_WasmConfigFileCandidates Include="@(StaticWebAsset)" Condition="'%(SourceType)' == 'Discovered'" />
<_WasmDiscoveredFileCandidates Include="@(StaticWebAsset)" Condition="'%(SourceType)' == 'Discovered'" />

<!-- Remove dotnet.js/wasm from runtime pack, in favor of the relinked ones in @(WasmNativeAsset) -->
<ReferenceCopyLocalPaths Remove="@(ReferenceCopyLocalPaths)" Condition="$(_WasmNativeAssetFileNames.Contains(';%(FileName)%(Extension);'))" />
Expand Down Expand Up @@ -319,14 +319,13 @@ Copyright (c) .NET Foundation. All rights reserved.
</DefineStaticWebAssets>

<DefineStaticWebAssets
CandidateAssets="@(_WasmConfigFileCandidates)"
CandidateAssets="@(_WasmDiscoveredFileCandidates)"
AssetTraitName="WasmResource"
AssetTraitValue="settings"
RelativePathFilter="appsettings*.json"
>
<Output TaskParameter="Assets" ItemName="_WasmJsConfigStaticWebAsset" />
</DefineStaticWebAssets>

<DefineStaticWebAssetEndpoints
CandidateAssets="@(_WasmJsConfigStaticWebAsset)"
ExistingEndpoints="@(StaticWebAssetEndpoint)"
Expand All @@ -335,12 +334,37 @@ Copyright (c) .NET Foundation. All rights reserved.
<Output TaskParameter="Endpoints" ItemName="_WasmJsConfigStaticWebAssetEndpoint" />
</DefineStaticWebAssetEndpoints>

<ItemGroup>
<WasmFilesToIncludeInFileSystem Condition="'%(WasmFilesToIncludeInFileSystem.TargetPath)' == ''">
<TargetPath>%(WasmFilesToIncludeInFileSystem.Identity)</TargetPath>
Copy link

Copilot AI Oct 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The TargetPath should be prefixed with '/' to ensure it's an absolute path in the VFS. According to the PR description, files should be loaded to paths like '/file.txt' and '/subdir/file.txt', but this sets TargetPath to 'file.txt' or 'subdir/file.txt' without the leading slash.

Suggested change
<TargetPath>%(WasmFilesToIncludeInFileSystem.Identity)</TargetPath>
<TargetPath>/%(WasmFilesToIncludeInFileSystem.Identity)</TargetPath>

Copilot uses AI. Check for mistakes.
</WasmFilesToIncludeInFileSystem>
</ItemGroup>

<DefineStaticWebAssets
CandidateAssets="@(_WasmDiscoveredFileCandidates)"
AssetTraitName="WasmResource"
AssetTraitValue="vfs:%(WasmFilesToIncludeInFileSystem.TargetPath)"
RelativePathFilter="@(WasmFilesToIncludeInFileSystem)"
Condition="'@(WasmFilesToIncludeInFileSystem)' != ''"
>
<Output TaskParameter="Assets" ItemName="_WasmFilesToIncludeInFileSystemStaticWebAsset" />
</DefineStaticWebAssets>
<DefineStaticWebAssetEndpoints
CandidateAssets="@(_WasmFilesToIncludeInFileSystemStaticWebAsset)"
ExistingEndpoints="@(StaticWebAssetEndpoint)"
ContentTypeMappings="@(StaticWebAssetContentTypeMapping)"
>
<Output TaskParameter="Endpoints" ItemName="_WasmFilesToIncludeInFileSystemStaticWebAssetEndpoint" />
</DefineStaticWebAssetEndpoints>

<ItemGroup>
<!-- Update the boot config static web asset since we've given it a trait -->
<StaticWebAsset Remove="@(_WasmJsConfigStaticWebAsset)" />
<StaticWebAsset Include="@(_WasmJsConfigStaticWebAsset)" />
<StaticWebAsset Remove="@(_WasmFilesToIncludeInFileSystemStaticWebAsset)" />
<StaticWebAsset Include="@(_WasmFilesToIncludeInFileSystemStaticWebAsset)" />
<StaticWebAssetEndpoint Include="@(_WasmJsConfigStaticWebAssetEndpoint)" />

<StaticWebAssetEndpoint Include="@(_WasmFilesToIncludeInFileSystemStaticWebAssetEndpoint)" />
<ReferenceCopyLocalPaths Remove="@(_WasmBuildFilesToRemove)" />
</ItemGroup>

Expand Down Expand Up @@ -385,8 +409,8 @@ Copyright (c) .NET Foundation. All rights reserved.
</DefineStaticWebAssetEndpoints>

<ResolveFingerprintedStaticWebAssetEndpointsForAssets
CandidateEndpoints="@(WasmStaticWebAssetEndpoint);@(_WasmJsModuleCandidatesForBuildEndpoint)"
CandidateAssets="@(WasmStaticWebAsset);@(_WasmJsModuleCandidatesForBuild)"
CandidateEndpoints="@(WasmStaticWebAssetEndpoint);@(_WasmJsModuleCandidatesForBuildEndpoint);@(StaticWebAssetEndpoint)"
CandidateAssets="@(WasmStaticWebAsset);@(_WasmJsModuleCandidatesForBuild);@(_WasmFilesToIncludeInFileSystemStaticWebAsset)"
IsStandalone="$(StaticWebAssetStandaloneHosting)"
>
<Output TaskParameter="ResolvedEndpoints" ItemName="_WasmResolvedEndpoints" />
Expand All @@ -395,7 +419,7 @@ Copyright (c) .NET Foundation. All rights reserved.
<GenerateWasmBootJson
AssemblyPath="@(IntermediateAssembly)"
ApplicationEnvironment="$(_WasmBuildApplicationEnvironmentName)"
Resources="@(WasmStaticWebAsset);@(_WasmJsModuleCandidatesForBuild)"
Resources="@(WasmStaticWebAsset);@(_WasmJsModuleCandidatesForBuild);@(_WasmFilesToIncludeInFileSystemStaticWebAsset)"
Endpoints="@(_WasmResolvedEndpoints)"
DebugBuild="true"
DebugLevel="$(WasmDebugLevel)"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// 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.Collections.Specialized;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Xunit;
using Xunit.Abstractions;

#nullable enable

namespace Wasm.Build.Tests;

public class FilesToIncludeInFileSystemTests : WasmTemplateTestsBase
{
public FilesToIncludeInFileSystemTests(ITestOutputHelper output, SharedBuildPerTestClassFixture buildContext)
: base(output, buildContext)
{
}

public static IEnumerable<object?[]> LoadFilesToVfsData()
{
if (!EnvironmentVariables.UseJavascriptBundler)
yield return new object?[] { false };

yield return new object?[] { true };
}

[Theory, TestCategory("bundler-friendly")]
[MemberData(nameof(LoadFilesToVfsData))]
public async Task LoadFilesToVfs(bool publish)
{
Configuration config = Configuration.Debug;
ProjectInfo info = CopyTestAsset(config, aot: false, TestAsset.WasmBasicTestApp, "FilesToIncludeInFileSystemTest");

if (publish)
PublishProject(info, config, new PublishOptions());
else
BuildProject(info, config, new BuildOptions());

BrowserRunOptions runOptions = new(
config,
TestScenario: "FilesToIncludeInFileSystemTest"
);
RunResult result = publish
? await RunForPublishWithWebServer(runOptions)
: await RunForBuildWithDotnetRun(runOptions);

Assert.Contains(result.TestOutput, m => m.Contains("'/myfiles/Vfs1.txt' exists 'True' with content 'Vfs1.txt'"));
Assert.Contains(result.TestOutput, m => m.Contains("'/myfiles/Vfs2.txt' exists 'True' with content 'Vfs2.txt'"));
Assert.Contains(result.TestOutput, m => m.Contains("'/subdir/subsubdir/Vfs3.txt' exists 'True' with content 'Vfs3.txt'"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export default {
}),
files({
output: 'public',
extensions: /\.(json)$/,
extensions: /\.(json|txt)$/,
}),
nodeResolve({
extensions: ['.js']
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// 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 System.Text.Json;
using System.Runtime.InteropServices.JavaScript;

public partial class FilesToIncludeInFileSystemTest
{
[JSExport]
public static void Run()
{
// Check file presence in VFS based on application environment
PrintFileExistence("/myfiles/Vfs1.txt");
PrintFileExistence("/myfiles/Vfs2.txt");
PrintFileExistence("/subdir/subsubdir/Vfs3.txt");
}

// Synchronize with FilesToIncludeInFileSystemTests
private static void PrintFileExistence(string path) => TestOutput.WriteLine($"'{path}' exists '{File.Exists(path)}' with content '{File.ReadAllText(path)}'");
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,10 @@
<BlazorWebAssemblyLazyLoad Include="LazyLibrary$(WasmAssemblyExtension)" />
<WasmExtraFilesToDeploy Include="profiler.js" />
</ItemGroup>

<ItemGroup>
<WasmFilesToIncludeInFileSystem Include="Vfs1.txt" TargetPath="/myfiles/Vfs1.txt" />
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What would happen if there is reference to non-existent file ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would get ignored. I could make it fail the build

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That may be better

<WasmFilesToIncludeInFileSystem Include="subdir/Vfs2.txt" TargetPath="/myfiles/Vfs2.txt" />
<WasmFilesToIncludeInFileSystem Include="subdir/subsubdir/Vfs3.txt" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Vfs1.txt
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,10 @@ try {
exports.AppSettingsTest.Run();
exit(0);
break;
case "FilesToIncludeInFileSystemTest":
exports.FilesToIncludeInFileSystemTest.Run();
exit(0);
break;
case "DownloadResourceProgressTest":
exit(0);
break;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Vfs2.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Vfs3.txt
Original file line number Diff line number Diff line change
Expand Up @@ -321,14 +321,14 @@ public string TransformResourcesToAssets(BootJsonData config, bool bundlerFriend
var asset = new VfsAsset()
{
virtualPath = a.Key,
name = a.Value.Keys.First(),
name = $"../{a.Value.Keys.First()}",
integrity = a.Value.Values.First()
};

if (bundlerFriendly)
{
string escaped = EscapeName(string.Concat(asset.name));
imports.Add($"import * as {escaped} from \"./{asset.name}\";");
imports.Add($"import {escaped} from \"./{asset.name}\";");
Copy link

Copilot AI Oct 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing the * as from the import statement changes the import from a namespace import to a default import. This breaks compatibility for modules that export a namespace rather than a default export. If VFS assets are expected to be modules with namespace exports, this change will cause runtime errors.

Suggested change
imports.Add($"import {escaped} from \"./{asset.name}\";");
imports.Add($"import * as {escaped} from \"./{asset.name}\";");

Copilot uses AI. Check for mistakes.
asset.resolvedUrl = EncodeJavascriptVariableInJson(escaped);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,8 @@ private void WriteBootConfig(string entryAssemblyName)
var assetTraitName = resource.GetMetadata("AssetTraitName");
var assetTraitValue = resource.GetMetadata("AssetTraitValue");
var resourceName = Path.GetFileName(resource.GetMetadata("OriginalItemSpec"));
var resourceRoute = Path.GetFileName(endpointByAsset[resource.ItemSpec].ItemSpec);
var resourceEndpoint = endpointByAsset[resource.ItemSpec].ItemSpec;
var resourceRoute = Path.GetFileName(resourceEndpoint);

if (TryGetLazyLoadedAssembly(lazyLoadAssembliesWithoutExtension, resourceName, out var lazyLoad))
{
Expand Down Expand Up @@ -354,6 +355,16 @@ private void WriteBootConfig(string entryAssemblyName)
AddResourceToList(resource, resourceList, targetPath);
continue;
}
else if (string.Equals("WasmResource", assetTraitName, StringComparison.OrdinalIgnoreCase) && assetTraitValue.StartsWith("vfs:", StringComparison.OrdinalIgnoreCase))
{
Log.LogMessage(MessageImportance.Low, "Candidate '{0}' is defined as VFS resource '{1}'.", resource.ItemSpec, assetTraitValue);

var targetPath = assetTraitValue.Substring("vfs:".Length);
Copy link

Copilot AI Oct 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use string slice operator assetTraitValue[\"vfs:\".Length..] instead of Substring for better performance and modern C# style.

Suggested change
var targetPath = assetTraitValue.Substring("vfs:".Length);
var targetPath = assetTraitValue["vfs:".Length..];

Copilot uses AI. Check for mistakes.

resourceData.vfs ??= [];
resourceData.vfs[targetPath] = [];
AddResourceToList(resource, resourceData.vfs[targetPath], resourceEndpoint);
}
else
{
Log.LogMessage(MessageImportance.Low, "Skipping resource '{0}' since it doesn't belong to a defined category.", resource.ItemSpec);
Expand Down Expand Up @@ -418,7 +429,6 @@ private void WriteBootConfig(string entryAssemblyName)
}
}


if (EnvVariables != null && EnvVariables.Length > 0)
{
result.environmentVariables = new Dictionary<string, string>();
Expand All @@ -428,6 +438,7 @@ private void WriteBootConfig(string entryAssemblyName)
result.environmentVariables[name] = env.GetMetadata("Value");
}
}

if (Extensions != null && Extensions.Length > 0)
{
result.extensions = new Dictionary<string, Dictionary<string, object>>();
Expand Down
3 changes: 2 additions & 1 deletion src/tasks/WasmAppBuilder/WasmAppBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,7 @@ protected override bool ExecuteInternal()
StringDictionary targetPathTable = new();
var vfs = new Dictionary<string, Dictionary<string, string>>();
var coreVfs = new Dictionary<string, Dictionary<string, string>>();
var virtualPathPrefix = !string.IsNullOrEmpty(RuntimeAssetsLocation) ? $"{RuntimeAssetsLocation}/supportFiles" : "supportFiles";
foreach (var item in FilesToIncludeInFileSystem)
{
string? targetPath = item.GetMetadata("TargetPath");
Expand Down Expand Up @@ -323,7 +324,7 @@ protected override bool ExecuteInternal()
};
vfsDict[targetPath] = new()
{
[$"supportFiles/{generatedFileName}"] = Utils.ComputeIntegrity(vfsPath)
[$"{virtualPathPrefix}/{generatedFileName}"] = Utils.ComputeIntegrity(vfsPath)
};
}

Expand Down
Loading