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
23 changes: 23 additions & 0 deletions src/mono/wasm/Wasm.Build.Tests/LazyLoadingTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,29 @@ public async Task LoadLazyAssemblyBeforeItIsNeeded(string lazyLoadingTestExtensi
}
}

[Fact]
public async Task LoadLazyAssemblyTwiceIsIdempotent()
{
// Regression test for the lazy loader rewriting an asset's virtualPath in place while
// fetching it, which broke the lookup (and dedup) on a subsequent load of the same
// assembly - e.g. when Blazor fires OnNavigate more than once - and surfaced as
// "<assembly> must be marked with 'BlazorWebAssemblyLazyLoad' item group ...".
Configuration config = Configuration.Debug;
ProjectInfo info = CopyTestAsset(config, false, TestAsset.WasmBasicTestApp, "LazyLoadingTests");
BuildProject(info, config, new BuildOptions(ExtraMSBuildArgs: "-p:TestLazyLoading=true"));

RunResult result = await RunForBuildWithDotnetRun(new BrowserRunOptions(
config,
TestScenario: "LazyLoadingTest",
BrowserQueryString: new NameValueCollection { { "loadLazyAssemblyTwice", "true" } }
));

Assert.True(result.TestOutput.Any(m => m.Contains("firstJsonLoad=true")), "The first lazy load should report that it loaded the assembly");
Assert.True(result.TestOutput.Any(m => m.Contains("secondJsonLoad=false")), "The second lazy load of the same assembly should be an idempotent no-op");
Assert.True(result.TestOutput.Any(m => m.Contains("FirstName")), "The lazy loading test didn't emit expected message with JSON");
Assert.False(result.ConsoleOutput.Any(m => m.Contains("must be marked with 'BlazorWebAssemblyLazyLoad'")), "Reloading an already-loaded lazy assembly must not throw the 'must be marked' error");
}

[Fact]
public async Task FailOnMissingLazyAssembly()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,15 @@ try {
break;
}

await INTERNAL.loadLazyAssembly(`Json${lazyAssemblyExtension}`);
const firstJsonLoad = await INTERNAL.loadLazyAssembly(`Json${lazyAssemblyExtension}`);
testOutput(`firstJsonLoad=${firstJsonLoad}`);
if (params.get("loadLazyAssemblyTwice") === "true") {
// Regression test: loading the same lazy assembly a second time must be an
// idempotent no-op (returns false) and must not throw
// "must be marked with 'BlazorWebAssemblyLazyLoad'".
const secondJsonLoad = await INTERNAL.loadLazyAssembly(`Json${lazyAssemblyExtension}`);
testOutput(`secondJsonLoad=${secondJsonLoad}`);
}
exports.LazyLoadingTest.Run();
await INTERNAL.loadLazyAssembly(`LazyLibrary${lazyAssemblyExtension}`);
const { LazyLibrary } = await getAssemblyExports("LazyLibrary");
Expand Down
21 changes: 13 additions & 8 deletions src/native/libs/Common/JavaScript/loader/assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,10 @@ export async function fetchSatelliteAssemblies(culturesToLoad: string[]): Promis
await Promise.all(promises);
}

function lazyAssetFileName(virtualPath: string): string {
return virtualPath.substring(virtualPath.lastIndexOf("/") + 1);
}

export async function fetchLazyAssembly(assemblyNameToLoad: string): Promise<boolean> {
const lazyAssemblies = loaderConfig.resources?.lazyAssembly;
if (!lazyAssemblies) {
Expand All @@ -273,12 +277,17 @@ export async function fetchLazyAssembly(assemblyNameToLoad: string): Promise<boo
else if (assemblyNameToLoad.endsWith(".wasm"))
assemblyNameWithoutExtension = assemblyNameToLoad.substring(0, assemblyNameToLoad.length - 5);

if (loadedLazyAssemblies.has(assemblyNameWithoutExtension)) {
return false;
}

const assemblyNameToLoadDll = assemblyNameWithoutExtension + ".dll";
const assemblyNameToLoadWasm = assemblyNameWithoutExtension + ".wasm";

let dllAsset: AssemblyAsset | null = null;
for (const asset of lazyAssemblies) {
if (asset.virtualPath === assemblyNameToLoadDll || asset.virtualPath === assemblyNameToLoadWasm) {
const fileName = lazyAssetFileName(asset.virtualPath);
if (fileName === assemblyNameToLoadDll || fileName === assemblyNameToLoadWasm) {
dllAsset = asset;
break;
}
Expand All @@ -288,28 +297,24 @@ export async function fetchLazyAssembly(assemblyNameToLoad: string): Promise<boo
throw new Error(`${assemblyNameToLoad} must be marked with 'BlazorWebAssemblyLazyLoad' item group in your project file to allow lazy-loading.`);
}

if (loadedLazyAssemblies.has(dllAsset.virtualPath)) {
return false;
}

await fetchAssembly(dllAsset);
loadedLazyAssemblies.add(dllAsset.virtualPath);
loadedLazyAssemblies.add(assemblyNameWithoutExtension);

if (loaderConfig.debugLevel !== 0) {
const pdbNameToLoad = assemblyNameWithoutExtension + ".pdb";
const pdbAssets = loaderConfig.resources?.pdb;
let pdbAssetToLoad: AssemblyAsset | undefined;
if (pdbAssets) {
for (const pdbAsset of pdbAssets) {
if (pdbAsset.virtualPath === pdbNameToLoad) {
if (lazyAssetFileName(pdbAsset.virtualPath) === pdbNameToLoad) {
pdbAssetToLoad = pdbAsset;
break;
}
}
}
if (!pdbAssetToLoad) {
for (const lazyAsset of lazyAssemblies) {
if (lazyAsset.virtualPath === pdbNameToLoad) {
if (lazyAssetFileName(lazyAsset.virtualPath) === pdbNameToLoad) {
pdbAssetToLoad = lazyAsset as AssemblyAsset;
break;
}
Expand Down
Loading