Skip to content

Commit 0d6d6c3

Browse files
authored
[browser] fix loading assets (#89687)
1 parent aaab170 commit 0d6d6c3

File tree

16 files changed

+306
-81
lines changed

16 files changed

+306
-81
lines changed

src/mono/sample/wasm/browser-advanced/main.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ try {
5252
},
5353
postRun: () => { console.log('user code Module.postRun'); },
5454
})
55+
.withResourceLoader((type, name, defaultUri, integrity, behavior) => {
56+
// loadBootResource could return string with unqualified name of resource. It assumes that we resolve it with document.baseURI
57+
return name;
58+
})
5559
.create();
5660

5761
// at this point both emscripten and monoVM are fully initialized.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.Runtime.InteropServices.JavaScript;
6+
using System.Runtime.InteropServices;
7+
8+
namespace Sample
9+
{
10+
public partial class Test
11+
{
12+
public static int Main(string[] args)
13+
{
14+
Console.WriteLine("Hello minimal config");
15+
return 0;
16+
}
17+
}
18+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<Import Project="..\DefaultBrowserSample.targets" />
3+
<ItemGroup>
4+
<WasmExtraFilesToDeploy Include="main.js" />
5+
</ItemGroup>
6+
</Project>
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<!DOCTYPE html>
2+
<!-- Licensed to the .NET Foundation under one or more agreements. -->
3+
<!-- The .NET Foundation licenses this file to you under the MIT license. -->
4+
<html>
5+
6+
<head>
7+
<title>Wasm Browser Sample</title>
8+
<meta charset="UTF-8">
9+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
10+
<script type='module' src="./main.js"></script>
11+
</head>
12+
13+
<body>
14+
<h3 id="header">Wasm Browser Minimal Config Sample</h3>
15+
</body>
16+
17+
</html>
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { dotnet } from "./_framework/dotnet.js";
2+
3+
async function fetchBinary(uri) {
4+
return new Uint8Array(await (await fetch(uri)).arrayBuffer());
5+
}
6+
7+
const assets = [
8+
{
9+
name: "dotnet.native.js",
10+
behavior: "js-module-native"
11+
},
12+
{
13+
name: "dotnet.js",
14+
behavior: "js-module-dotnet"
15+
},
16+
{
17+
name: "dotnet.runtime.js",
18+
behavior: "js-module-runtime"
19+
},
20+
{
21+
name: "dotnet.native.wasm",
22+
behavior: "dotnetwasm"
23+
},
24+
{
25+
name: "System.Private.CoreLib.wasm",
26+
behavior: "assembly"
27+
},
28+
{
29+
name: "System.Runtime.InteropServices.JavaScript.wasm",
30+
behavior: "assembly"
31+
},
32+
{
33+
name: "Wasm.Browser.Config.Sample.wasm",
34+
buffer: await fetchBinary("./_framework/Wasm.Browser.Config.Sample.wasm"),
35+
behavior: "assembly"
36+
},
37+
{
38+
name: "System.Console.wasm",
39+
behavior: "assembly"
40+
},
41+
];
42+
43+
const resources = {
44+
"jsModuleNative": {
45+
"dotnet.native.js": ""
46+
},
47+
"jsModuleRuntime": {
48+
"dotnet.runtime.js": ""
49+
},
50+
"wasmNative": {
51+
"dotnet.native.wasm": ""
52+
},
53+
"assembly": {
54+
"System.Console.wasm": "",
55+
"System.Private.CoreLib.wasm": "",
56+
"System.Runtime.InteropServices.JavaScript.wasm": "",
57+
"Wasm.Browser.Config.Sample.wasm": ""
58+
},
59+
};
60+
61+
const config = {
62+
"mainAssemblyName": "Wasm.Browser.Config.Sample.dll",
63+
assets,
64+
};
65+
66+
await dotnet
67+
.withConfig(config)
68+
.withElementOnExit()
69+
.withExitCodeLogging()
70+
.run();

src/mono/wasm/Wasm.Build.Tests/ProjectProviderBase.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -393,7 +393,7 @@ public void AssertBootJson(AssertBundleOptionsBase options)
393393
var bootJsonEntries = bootJson.resources.jsModuleNative.Keys
394394
.Union(bootJson.resources.jsModuleRuntime.Keys)
395395
.Union(bootJson.resources.jsModuleWorker?.Keys ?? Enumerable.Empty<string>())
396-
.Union(bootJson.resources.jsSymbols?.Keys ?? Enumerable.Empty<string>())
396+
.Union(bootJson.resources.wasmSymbols?.Keys ?? Enumerable.Empty<string>())
397397
.Union(bootJson.resources.wasmNative.Keys)
398398
.ToArray();
399399

src/mono/wasm/runtime/dotnet.d.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ interface ResourceGroups {
181181
jsModuleWorker?: ResourceList;
182182
jsModuleNative: ResourceList;
183183
jsModuleRuntime: ResourceList;
184-
jsSymbols?: ResourceList;
184+
wasmSymbols?: ResourceList;
185185
wasmNative: ResourceList;
186186
icu?: ResourceList;
187187
satelliteResources?: {
@@ -207,9 +207,11 @@ type ResourceList = {
207207
* @param name The name of the resource to be loaded.
208208
* @param defaultUri The URI from which the framework would fetch the resource by default. The URI may be relative or absolute.
209209
* @param integrity The integrity string representing the expected content in the response.
210+
* @param behavior The detailed behavior/type of the resource to be loaded.
210211
* @returns A URI string or a Response promise to override the loading process, or null/undefined to allow the default loading behavior.
212+
* When returned string is not qualified with `./` or absolute URL, it will be resolved against the application base URI.
211213
*/
212-
type LoadBootResourceCallback = (type: AssetBehaviors | "manifest", name: string, defaultUri: string, integrity: string) => string | Promise<Response> | null | undefined;
214+
type LoadBootResourceCallback = (type: WebAssemblyBootResourceType, name: string, defaultUri: string, integrity: string, behavior: AssetBehaviors) => string | Promise<Response> | null | undefined;
213215
interface ResourceRequest {
214216
name: string;
215217
behavior: AssetBehaviors;
@@ -254,18 +256,26 @@ type SingleAssetBehaviors =
254256
* The binary of the dotnet runtime.
255257
*/
256258
"dotnetwasm"
259+
/**
260+
* The javascript module for loader.
261+
*/
262+
| "js-module-dotnet"
257263
/**
258264
* The javascript module for threads.
259265
*/
260266
| "js-module-threads"
261267
/**
262-
* The javascript module for threads.
268+
* The javascript module for runtime.
263269
*/
264270
| "js-module-runtime"
265271
/**
266-
* The javascript module for threads.
272+
* The javascript module for emscripten.
273+
*/
274+
| "js-module-native"
275+
/**
276+
* Typically blazor.boot.json
267277
*/
268-
| "js-module-native";
278+
| "manifest";
269279
type AssetBehaviors = SingleAssetBehaviors |
270280
/**
271281
* Load asset as a managed resource assembly.
@@ -396,6 +406,7 @@ type ModuleAPI = {
396406
exit: (code: number, reason?: any) => void;
397407
};
398408
type CreateDotnetRuntimeType = (moduleFactory: DotnetModuleConfig | ((api: RuntimeAPI) => DotnetModuleConfig)) => Promise<RuntimeAPI>;
409+
type WebAssemblyBootResourceType = "assembly" | "pdb" | "dotnetjs" | "dotnetwasm" | "globalization" | "manifest" | "configuration";
399410

400411
interface IDisposable {
401412
dispose(): void;
@@ -431,4 +442,4 @@ declare global {
431442
}
432443
declare const createDotnetRuntime: CreateDotnetRuntimeType;
433444

434-
export { AssetEntry, CreateDotnetRuntimeType, DotnetHostBuilder, DotnetModuleConfig, EmscriptenModule, GlobalizationMode, IMemoryView, ModuleAPI, MonoConfig, ResourceRequest, RuntimeAPI, createDotnetRuntime as default, dotnet, exit };
445+
export { AssetBehaviors, AssetEntry, CreateDotnetRuntimeType, DotnetHostBuilder, DotnetModuleConfig, EmscriptenModule, GlobalizationMode, IMemoryView, ModuleAPI, MonoConfig, ResourceRequest, RuntimeAPI, createDotnetRuntime as default, dotnet, exit };

src/mono/wasm/runtime/loader/assets.ts

Lines changed: 44 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import { mono_log_debug } from "./logging";
99
import { mono_exit } from "./exit";
1010
import { addCachedReponse, findCachedResponse, isCacheAvailable } from "./assetsCache";
1111
import { getIcuResourceName } from "./icu";
12+
import { mono_log_warn } from "./logging";
13+
import { makeURLAbsoluteWithApplicationBase } from "./polyfills";
1214

1315

1416
let throttlingPromise: PromiseAndController<void> | undefined;
@@ -20,6 +22,7 @@ const jsModulesAssetTypes: {
2022
} = {
2123
"js-module-threads": true,
2224
"js-module-runtime": true,
25+
"js-module-dotnet": true,
2326
"js-module-native": true,
2427
};
2528

@@ -78,10 +81,10 @@ function getSingleAssetWithResolvedUrl(resources: ResourceList | undefined, beha
7881

7982
const customSrc = invokeLoadBootResource(asset);
8083
if (typeof (customSrc) === "string") {
81-
asset.resolvedUrl = customSrc;
84+
asset.resolvedUrl = makeURLAbsoluteWithApplicationBase(customSrc);
8285
} else if (customSrc) {
83-
// Since we must load this via a import, it's only valid to supply a URI (and not a Request, say)
84-
throw new Error(`For a ${behavior} resource, custom loaders must supply a URI string.`);
86+
mono_log_warn(`For ${behavior} resource: ${name}, custom loaders must supply a URI string.`);
87+
// we apply a default URL
8588
}
8689

8790
return asset;
@@ -164,8 +167,9 @@ export async function mono_download_assets(): Promise<void> {
164167
const asset = await downloadPromise;
165168
if (asset.buffer) {
166169
if (!skipInstantiateByAssetTypes[asset.behavior]) {
167-
const url = asset.pendingDownloadInternal!.url;
168170
mono_assert(asset.buffer && typeof asset.buffer === "object", "asset buffer must be array or buffer like");
171+
mono_assert(typeof asset.resolvedUrl === "string", "resolvedUrl must be string");
172+
const url = asset.resolvedUrl!;
169173
const data = new Uint8Array(asset.buffer!);
170174
cleanupAsset(asset);
171175

@@ -220,8 +224,25 @@ export async function mono_download_assets(): Promise<void> {
220224

221225
function prepareAssets(containedInSnapshotAssets: AssetEntryInternal[], alwaysLoadedAssets: AssetEntryInternal[]) {
222226
const config = loaderHelpers.config;
223-
const resources = loaderHelpers.config.resources;
224-
if (resources) {
227+
228+
// if assets exits, we will assume Net7 legacy and not process resources object
229+
if (config.assets) {
230+
for (const a of config.assets) {
231+
const asset: AssetEntryInternal = a;
232+
mono_assert(typeof asset === "object", () => `asset must be object, it was ${typeof asset} : ${asset}`);
233+
mono_assert(typeof asset.behavior === "string", "asset behavior must be known string");
234+
mono_assert(typeof asset.name === "string", "asset name must be string");
235+
mono_assert(!asset.resolvedUrl || typeof asset.resolvedUrl === "string", "asset resolvedUrl could be string");
236+
mono_assert(!asset.hash || typeof asset.hash === "string", "asset resolvedUrl could be string");
237+
mono_assert(!asset.pendingDownload || typeof asset.pendingDownload === "object", "asset pendingDownload could be object");
238+
if (containedInSnapshotByAssetTypes[asset.behavior]) {
239+
containedInSnapshotAssets.push(asset);
240+
} else {
241+
alwaysLoadedAssets.push(asset);
242+
}
243+
}
244+
} else if (config.resources) {
245+
const resources = config.resources;
225246
if (resources.assembly) {
226247
for (const name in resources.assembly) {
227248
containedInSnapshotAssets.push({
@@ -282,11 +303,11 @@ function prepareAssets(containedInSnapshotAssets: AssetEntryInternal[], alwaysLo
282303
}
283304
}
284305

285-
if (resources.jsSymbols) {
286-
for (const name in resources.jsSymbols) {
306+
if (resources.wasmSymbols) {
307+
for (const name in resources.wasmSymbols) {
287308
alwaysLoadedAssets.push({
288309
name,
289-
hash: resources.jsSymbols[name],
310+
hash: resources.wasmSymbols[name],
290311
behavior: "symbols"
291312
});
292313
}
@@ -307,31 +328,7 @@ function prepareAssets(containedInSnapshotAssets: AssetEntryInternal[], alwaysLo
307328
}
308329
}
309330

310-
const newAssets = [...containedInSnapshotAssets, ...alwaysLoadedAssets];
311-
312-
if (loaderHelpers.config.assets) {
313-
for (const a of loaderHelpers.config.assets) {
314-
const asset: AssetEntryInternal = a;
315-
mono_assert(typeof asset === "object", "asset must be object");
316-
mono_assert(typeof asset.behavior === "string", "asset behavior must be known string");
317-
mono_assert(typeof asset.name === "string", "asset name must be string");
318-
mono_assert(!asset.resolvedUrl || typeof asset.resolvedUrl === "string", "asset resolvedUrl could be string");
319-
mono_assert(!asset.hash || typeof asset.hash === "string", "asset resolvedUrl could be string");
320-
mono_assert(!asset.pendingDownload || typeof asset.pendingDownload === "object", "asset pendingDownload could be object");
321-
if (containedInSnapshotByAssetTypes[asset.behavior]) {
322-
containedInSnapshotAssets.push(asset);
323-
} else {
324-
alwaysLoadedAssets.push(asset);
325-
}
326-
}
327-
}
328-
329-
if (!loaderHelpers.config.assets) {
330-
loaderHelpers.config.assets = [];
331-
}
332-
333-
loaderHelpers.config.assets = [...loaderHelpers.config.assets, ...newAssets];
334-
331+
config.assets = [...containedInSnapshotAssets, ...alwaysLoadedAssets];
335332
}
336333

337334
export function delay(ms: number): Promise<void> {
@@ -431,12 +428,17 @@ async function start_asset_download_sources(asset: AssetEntryInternal): Promise<
431428
}
432429
if (asset.buffer) {
433430
const buffer = asset.buffer;
434-
asset.buffer = null as any; // GC
431+
if (!asset.resolvedUrl) {
432+
asset.resolvedUrl = "undefined://" + asset.name;
433+
}
435434
asset.pendingDownloadInternal = {
436-
url: "undefined://" + asset.name,
435+
url: asset.resolvedUrl,
437436
name: asset.name,
438437
response: Promise.resolve({
438+
ok: true,
439439
arrayBuffer: () => buffer,
440+
json: () => JSON.parse(new TextDecoder("utf-8").decode(buffer)),
441+
text: () => { throw new Error("NotImplementedException"); },
440442
headers: {
441443
get: () => undefined,
442444
}
@@ -585,8 +587,7 @@ function fetchResource(request: ResourceRequest): Promise<Response> {
585587
// They are supplying an entire custom response, so just use that
586588
return customLoadResult;
587589
} else if (typeof customLoadResult === "string") {
588-
// They are supplying a custom URL, so use that with the default fetch behavior
589-
url = customLoadResult;
590+
url = makeURLAbsoluteWithApplicationBase(customLoadResult);
590591
}
591592
}
592593

@@ -614,7 +615,9 @@ const monoToBlazorAssetTypeMap: { [key: string]: WebAssemblyBootResourceType | u
614615
"pdb": "pdb",
615616
"icu": "globalization",
616617
"vfs": "configuration",
618+
"manifest": "manifest",
617619
"dotnetwasm": "dotnetwasm",
620+
"js-module-dotnet": "dotnetjs",
618621
"js-module-native": "dotnetjs",
619622
"js-module-runtime": "dotnetjs",
620623
"js-module-threads": "dotnetjs"
@@ -625,17 +628,10 @@ function invokeLoadBootResource(request: ResourceRequest): string | Promise<Resp
625628
const requestHash = request.hash ?? "";
626629
const url = request.resolvedUrl!;
627630

628-
// Try to send with AssetBehaviors
629-
let customLoadResult = loaderHelpers.loadBootResource(request.behavior, request.name, url, requestHash);
630-
if (!customLoadResult) {
631-
// If we don't get result, try to send with WebAssemblyBootResourceType
632-
const resourceType = monoToBlazorAssetTypeMap[request.behavior];
633-
if (resourceType) {
634-
customLoadResult = loaderHelpers.loadBootResource(resourceType as AssetBehaviors, request.name, url, requestHash);
635-
}
631+
const resourceType = monoToBlazorAssetTypeMap[request.behavior];
632+
if (resourceType) {
633+
return loaderHelpers.loadBootResource(resourceType, request.name, url, requestHash, request.behavior);
636634
}
637-
638-
return customLoadResult;
639635
}
640636

641637
return undefined;

0 commit comments

Comments
 (0)