Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[wasm] Unify boot config location #85763

Merged
merged 11 commits into from
May 12, 2023
2 changes: 1 addition & 1 deletion src/mono/sample/wasm/browser-advanced/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script type='module' src="./main.js"></script>
<script type='module' src="./dotnet.js"></script>
<link rel="preload" href="./mono-config.json" as="fetch" crossorigin="anonymous">
<link rel="preload" href="./_framework/blazor.boot.json" as="fetch" crossorigin="anonymous">
<link rel="prefetch" href="./dotnet.native.js" as="fetch" crossorigin="anonymous">
<link rel="prefetch" href="./dotnet.runtime.js" as="fetch" crossorigin="anonymous">
<link rel="prefetch" href="./dotnet.native.wasm" as="fetch" crossorigin="anonymous">
Expand Down
2 changes: 1 addition & 1 deletion src/mono/sample/wasm/browser-advanced/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ try {
// here we show how emscripten could be further configured
// It is prefered to use specific 'with***' methods instead in all other cases.
.withModuleConfig({
configSrc: "./mono-config.json",
configSrc: "./_framework/blazor.boot.json",
onConfigLoaded: (config) => {
// This is called during emscripten `dotnet.wasm` instantiation, after we fetched config.
console.log('user code Module.onConfigLoaded');
Expand Down
2 changes: 1 addition & 1 deletion src/mono/sample/wasm/browser-bench/appstart-frame.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script type="module" src="./frame-main.js"></script>
<script type='module' src="./dotnet.js"></script>
<link rel="preload" href="./mono-config.json" as="fetch" crossorigin="anonymous">
<link rel="preload" href="./_framework/blazor.boot.json" as="fetch" crossorigin="anonymous">
<link rel="prefetch" href="./dotnet.native.js" as="fetch" crossorigin="anonymous">
<link rel="prefetch" href="./dotnet.runtime.js" as="fetch" crossorigin="anonymous">
<link rel="prefetch" href="./dotnet.native.wasm" as="fetch" crossorigin="anonymous">
Expand Down
2 changes: 1 addition & 1 deletion src/mono/wasi/Wasi.Build.Tests/BuildTestBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,7 @@ protected static void AssertBasicAppBundle(string bundleDir,
"index.html",
mainJS,
"dotnet.wasm",
"mono-config.json",
"_framework/blazor.boot.json",
"dotnet.js"
});

Expand Down
111 changes: 56 additions & 55 deletions src/mono/wasm/Wasm.Build.Tests/BuildTestBase.cs

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions src/mono/wasm/Wasm.Build.Tests/ConfigSrcTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ namespace Wasm.Build.Tests;
public class ConfigSrcTests : BuildTestBase
{
public ConfigSrcTests(ITestOutputHelper output, SharedBuildPerTestClassFixture buildContext) : base(output, buildContext)
{}
{ }

// NOTE: port number determinizes dynamically, so could not generate absolute URI
[Theory]
Expand All @@ -30,7 +30,7 @@ public void ConfigSrcAbsolutePath(BuildArgs buildArgs, RunHost host, string id)

string binDir = GetBinDir(baseDir: _projectDir!, config: buildArgs.Config);
string bundleDir = Path.Combine(binDir, "AppBundle");
string configSrc = Path.GetFullPath(Path.Combine(bundleDir, "mono-config.json"));
string configSrc = Path.GetFullPath(Path.Combine(bundleDir, "_framework", "blazor.boot.json"));

RunAndTestWasmApp(buildArgs, expectedExitCode: 42, host: host, id: id, extraXHarnessMonoArgs: $"--config-src={configSrc}");
}
Expand Down
2 changes: 1 addition & 1 deletion src/mono/wasm/build/WasmApp.targets
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@
- @(WasmFilesToIncludeInFileSystem) - Files to include in the vfs
- @(WasmNativeAsset) - Native files to be added to `NativeAssets` in the bundle.

- @(WasmExtraConfig) - json elements to add to `mono-config.json`
- @(WasmExtraConfig) - json elements to add to `_framework/blazor.boot.json`
Eg. <WasmExtraConfig Include="xxx" Value="true" />

- Value attribute can have a number, bool, quoted string, or json string
Expand Down
95 changes: 95 additions & 0 deletions src/mono/wasm/runtime/blazor/BootConfig.ts
maraf marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

import { Module } from "../globals";
import { WebAssemblyBootResourceType } from "../types-api";

type LoadBootResourceCallback = (type: WebAssemblyBootResourceType, name: string, defaultUri: string, integrity: string) => string | Promise<Response> | null | undefined;

export class BootConfigResult {
private constructor(public bootConfig: BootJsonData, public applicationEnvironment: string) {
}

static fromFetchResponse(bootConfigResponse: Response, bootConfig: BootJsonData, environment?: string): BootConfigResult {
const applicationEnvironment = environment || (Module.getApplicationEnvironment && Module.getApplicationEnvironment(bootConfigResponse)) || "Production";
bootConfig.modifiableAssemblies = bootConfigResponse.headers.get("DOTNET-MODIFIABLE-ASSEMBLIES");
bootConfig.aspnetCoreBrowserTools = bootConfigResponse.headers.get("ASPNETCORE-BROWSER-TOOLS");

return new BootConfigResult(bootConfig, applicationEnvironment);
}

static async initAsync(loadBootResource?: LoadBootResourceCallback, environment?: string): Promise<BootConfigResult> {
const defaultBootJsonLocation = "_framework/blazor.boot.json";

const loaderResponse = loadBootResource !== undefined ?
loadBootResource("manifest", "blazor.boot.json", defaultBootJsonLocation, "") :
defaultLoadBlazorBootJson(defaultBootJsonLocation);

let bootConfigResponse: Response;

if (!loaderResponse) {
bootConfigResponse = await defaultLoadBlazorBootJson(defaultBootJsonLocation);
} else if (typeof loaderResponse === "string") {
bootConfigResponse = await defaultLoadBlazorBootJson(loaderResponse);
} else {
bootConfigResponse = await loaderResponse;
}

const bootConfig: BootJsonData = await bootConfigResponse.json();
return BootConfigResult.fromFetchResponse(bootConfigResponse, bootConfig, environment);

function defaultLoadBlazorBootJson(url: string): Promise<Response> {
return fetch(url, {
method: "GET",
credentials: "include",
cache: "no-cache",
});
}
}
}

// Keep in sync with Microsoft.NET.Sdk.WebAssembly.BootJsonData from the WasmSDK
export interface BootJsonData {
readonly entryAssembly: string;
readonly resources: ResourceGroups;
/** Gets a value that determines if this boot config was produced from a non-published build (i.e. dotnet build or dotnet run) */
readonly debugBuild: boolean;
readonly linkerEnabled: boolean;
readonly cacheBootResources: boolean;
readonly config: string[];
readonly icuDataMode: ICUDataMode;
readonly startupMemoryCache: boolean | undefined;
readonly runtimeOptions: string[] | undefined;

// These properties are tacked on, and not found in the boot.json file
modifiableAssemblies: string | null;
aspnetCoreBrowserTools: string | null;
}

export type BootJsonDataExtension = { [extensionName: string]: ResourceList };

export interface ResourceGroups {
readonly assembly: ResourceList;
readonly lazyAssembly: ResourceList;
readonly pdb?: ResourceList;
readonly runtime: ResourceList;
readonly satelliteResources?: { [cultureName: string]: ResourceList };
readonly libraryInitializers?: ResourceList,
readonly extensions?: BootJsonDataExtension
readonly runtimeAssets: ExtendedResourceList;
}

export type ResourceList = { [name: string]: string };
export type ExtendedResourceList = {
[name: string]: {
hash: string,
behavior: string
}
};

export enum ICUDataMode {
Sharded,
All,
Invariant,
Custom
}
224 changes: 224 additions & 0 deletions src/mono/wasm/runtime/blazor/_Integration.ts
maraf marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
import { INTERNAL, Module } from "../globals";
import { MonoConfigInternal } from "../types";
import { AssetEntry, LoadingResource, WebAssemblyBootResourceType, WebAssemblyStartOptions } from "../types-api";
import { BootConfigResult, BootJsonData, ICUDataMode } from "./BootConfig";
import { WebAssemblyResourceLoader } from "./WebAssemblyResourceLoader";
import { hasDebuggingEnabled } from "./_Polyfill";

export async function loadBootConfig(config: MonoConfigInternal) {
const candidateOptions = config.startupOptions ?? {};
const environment = candidateOptions.environment;
const bootConfigPromise = BootConfigResult.initAsync(candidateOptions.loadBootResource, environment);

const bootConfigResult: BootConfigResult = await bootConfigPromise;

await initializeBootConfig(bootConfigResult, candidateOptions);
}

export async function initializeBootConfig(bootConfigResult: BootConfigResult, startupOptions?: Partial<WebAssemblyStartOptions>) {
const resourceLoader = await WebAssemblyResourceLoader.initAsync(bootConfigResult.bootConfig, startupOptions || {});

INTERNAL.resourceLoader = resourceLoader;

const newConfig = mapBootConfigToMonoConfig(Module.config as MonoConfigInternal, resourceLoader, bootConfigResult.applicationEnvironment);
Module.config = newConfig;
}

let resourcesLoaded = 0;
let totalResources = 0;

function mapBootConfigToMonoConfig(moduleConfig: MonoConfigInternal, resourceLoader: WebAssemblyResourceLoader, applicationEnvironment: string): MonoConfigInternal {
const resources = resourceLoader.bootConfig.resources;

const assets: AssetEntry[] = [];
const environmentVariables: any = {};

moduleConfig.applicationEnvironment = applicationEnvironment;

moduleConfig.assets = assets;
moduleConfig.globalizationMode = "icu";
moduleConfig.environmentVariables = environmentVariables;
moduleConfig.debugLevel = hasDebuggingEnabled(resourceLoader.bootConfig) ? 1 : 0;
moduleConfig.maxParallelDownloads = 1000000; // disable throttling parallel downloads
moduleConfig.enableDownloadRetry = false; // disable retry downloads
moduleConfig.mainAssemblyName = resourceLoader.bootConfig.entryAssembly;

moduleConfig = {
...resourceLoader.bootConfig,
...moduleConfig
};

if (resourceLoader.bootConfig.startupMemoryCache !== undefined) {
moduleConfig.startupMemoryCache = resourceLoader.bootConfig.startupMemoryCache;
}

if (resourceLoader.bootConfig.runtimeOptions) {
moduleConfig.runtimeOptions = [...(moduleConfig.runtimeOptions || []), ...resourceLoader.bootConfig.runtimeOptions];
}

const monoToBlazorAssetTypeMap: { [key: string]: WebAssemblyBootResourceType | undefined } = {
"assembly": "assembly",
"pdb": "pdb",
"icu": "globalization",
"vfs": "globalization",
"dotnetwasm": "dotnetwasm",
};

const behaviorByName = (name: string) => {
return name === "dotnet.timezones.blat" ? "vfs"
: name === "dotnet.wasm" ? "dotnetwasm"
: (name.startsWith("dotnet.worker") && name.endsWith(".js")) ? "js-module-threads"
: (name.startsWith("dotnet") && name.endsWith(".js")) ? "js-module-dotnet"
: name.startsWith("icudt") ? "icu"
: "other";
};

// it would not `loadResource` on types for which there is no typesMap mapping
const downloadResource = (asset: AssetEntry): LoadingResource | undefined => {
// GOTCHA: the mapping to blazor asset type may not cover all mono owned asset types in the future in which case:
// A) we may need to add such asset types to the mapping and to WebAssemblyBootResourceType
// B) or we could add generic "runtime" type to WebAssemblyBootResourceType as fallback
// C) or we could return `undefined` and let the runtime to load the asset. In which case the progress will not be reported on it and blazor will not be able to cache it.
const type = monoToBlazorAssetTypeMap[asset.behavior];
if (type !== undefined) {
const res = resourceLoader.loadResource(asset.name, asset.resolvedUrl!, asset.hash!, type);
asset.pendingDownload = res;

totalResources++;
res.response.then(() => {
resourcesLoaded++;
if (Module.onDownloadResourceProgress)
Module.onDownloadResourceProgress(resourcesLoaded, totalResources);
});

return res;
}
return undefined;
};

Module.downloadResource = downloadResource;
Module.disableDotnet6Compatibility = false;

// any runtime owned assets, with proper behavior already set
for (const name in resources.runtimeAssets) {
const asset = resources.runtimeAssets[name] as AssetEntry;
asset.name = name;
asset.resolvedUrl = `_framework/${name}`;
assets.push(asset);
if (asset.behavior === "dotnetwasm") {
downloadResource(asset);
}
}
for (const name in resources.assembly) {
const asset: AssetEntry = {
name,
resolvedUrl: `_framework/${name}`,
hash: resources.assembly[name],
behavior: "assembly",
};
assets.push(asset);
downloadResource(asset);
}
if (hasDebuggingEnabled(resourceLoader.bootConfig) && resources.pdb) {
for (const name in resources.pdb) {
const asset: AssetEntry = {
name,
resolvedUrl: `_framework/${name}`,
hash: resources.pdb[name],
behavior: "pdb",
};
assets.push(asset);
downloadResource(asset);
}
}
const applicationCulture = resourceLoader.startOptions.applicationCulture || (navigator.languages && navigator.languages[0]);
const icuDataResourceName = getICUResourceName(resourceLoader.bootConfig, applicationCulture);
let hasIcuData = false;
for (const name in resources.runtime) {
const behavior = behaviorByName(name) as any;
if (behavior === "icu") {
if (resourceLoader.bootConfig.icuDataMode === ICUDataMode.Invariant) {
continue;
}
if (name !== icuDataResourceName) {
continue;
}
hasIcuData = true;
} else if (behavior === "js-module-dotnet") {
continue;
} else if (behavior === "dotnetwasm") {
continue;
}
const asset: AssetEntry = {
name,
resolvedUrl: `_framework/${name}`,
hash: resources.runtime[name],
behavior,
};
assets.push(asset);
}

if (!hasIcuData) {
moduleConfig.globalizationMode = "invariant";
}

if (resourceLoader.bootConfig.modifiableAssemblies) {
// Configure the app to enable hot reload in Development.
environmentVariables["DOTNET_MODIFIABLE_ASSEMBLIES"] = resourceLoader.bootConfig.modifiableAssemblies;
}

if (resourceLoader.startOptions.applicationCulture) {
// If a culture is specified via start options use that to initialize the Emscripten \ .NET culture.
environmentVariables["LANG"] = `${resourceLoader.startOptions.applicationCulture}.UTF-8`;
}

if (resourceLoader.bootConfig.startupMemoryCache !== undefined) {
moduleConfig.startupMemoryCache = resourceLoader.bootConfig.startupMemoryCache;
}

if (resourceLoader.bootConfig.runtimeOptions) {
moduleConfig.runtimeOptions = [...(moduleConfig.runtimeOptions || []), ...(resourceLoader.bootConfig.runtimeOptions || [])];
}

return moduleConfig;
}

function getICUResourceName(bootConfig: BootJsonData, culture: string | undefined): string {
if (bootConfig.icuDataMode === ICUDataMode.Custom) {
const icuFiles = Object
.keys(bootConfig.resources.runtime)
.filter(n => n.startsWith("icudt") && n.endsWith(".dat"));
if (icuFiles.length === 1) {
const customIcuFile = icuFiles[0];
return customIcuFile;
}
}

const combinedICUResourceName = "icudt.dat";
if (!culture || bootConfig.icuDataMode === ICUDataMode.All) {
return combinedICUResourceName;
}

const prefix = culture.split("-")[0];
if (prefix === "en" ||
[
"fr",
"fr-FR",
"it",
"it-IT",
"de",
"de-DE",
"es",
"es-ES",
].includes(culture)) {
return "icudt_EFIGS.dat";
}
if ([
"zh",
"ko",
"ja",
].includes(prefix)) {
return "icudt_CJK.dat";
}
return "icudt_no_CJK.dat";
}
Loading