diff --git a/src/mono/wasm/runtime/blazor/BootConfig.ts b/src/mono/wasm/runtime/blazor/BootConfig.ts new file mode 100644 index 00000000000000..5100603f64c928 --- /dev/null +++ b/src/mono/wasm/runtime/blazor/BootConfig.ts @@ -0,0 +1,89 @@ +// 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 "../imports"; +import { WebAssemblyBootResourceType } from "./WebAssemblyStartOptions"; + +type LoadBootResourceCallback = (type: WebAssemblyBootResourceType, name: string, defaultUri: string, integrity: string) => string | Promise | null | undefined; + +export class BootConfigResult { + private constructor(public bootConfig: BootJsonData, public applicationEnvironment: string) { + } + + static async initAsync(loadBootResource?: LoadBootResourceCallback, environment?: string): Promise { + const loaderResponse = loadBootResource !== undefined ? + loadBootResource("manifest", "blazor.boot.json", "_framework/blazor.boot.json", "") : + defaultLoadBlazorBootJson("_framework/blazor.boot.json"); + + let bootConfigResponse: Response; + + if (!loaderResponse) { + bootConfigResponse = await defaultLoadBlazorBootJson("_framework/blazor.boot.json"); + } else if (typeof loaderResponse === "string") { + bootConfigResponse = await defaultLoadBlazorBootJson(loaderResponse); + } else { + bootConfigResponse = await loaderResponse; + } + + const applicationEnvironment = environment || (Module.getApplicationEnvironment && Module.getApplicationEnvironment(bootConfigResponse)) || "Production"; + const bootConfig: BootJsonData = await bootConfigResponse.json(); + bootConfig.modifiableAssemblies = bootConfigResponse.headers.get("DOTNET-MODIFIABLE-ASSEMBLIES"); + bootConfig.aspnetCoreBrowserTools = bootConfigResponse.headers.get("ASPNETCORE-BROWSER-TOOLS"); + + return new BootConfigResult(bootConfig, applicationEnvironment); + + function defaultLoadBlazorBootJson(url: string): Promise { + 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 +} diff --git a/src/mono/wasm/runtime/blazor/WebAssemblyResourceLoader.ts b/src/mono/wasm/runtime/blazor/WebAssemblyResourceLoader.ts new file mode 100644 index 00000000000000..73e363693cd7e7 --- /dev/null +++ b/src/mono/wasm/runtime/blazor/WebAssemblyResourceLoader.ts @@ -0,0 +1,223 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import { toAbsoluteUri } from "./_Polyfill"; +import { BootJsonData, ResourceList } from "./BootConfig"; +import { WebAssemblyStartOptions, WebAssemblyBootResourceType } from "./WebAssemblyStartOptions"; +const networkFetchCacheMode = "no-cache"; + +export class WebAssemblyResourceLoader { + private usedCacheKeys: { [key: string]: boolean } = {}; + + private networkLoads: { [name: string]: LoadLogEntry } = {}; + + private cacheLoads: { [name: string]: LoadLogEntry } = {}; + + static async initAsync(bootConfig: BootJsonData, startOptions: Partial): Promise { + const cache = await getCacheToUseIfEnabled(bootConfig); + return new WebAssemblyResourceLoader(bootConfig, cache, startOptions); + } + + constructor(readonly bootConfig: BootJsonData, readonly cacheIfUsed: Cache | null, readonly startOptions: Partial) { + } + + loadResources(resources: ResourceList, url: (name: string) => string, resourceType: WebAssemblyBootResourceType): LoadingResource[] { + return Object.keys(resources) + .map(name => this.loadResource(name, url(name), resources[name], resourceType)); + } + + loadResource(name: string, url: string, contentHash: string, resourceType: WebAssemblyBootResourceType): LoadingResource { + const response = this.cacheIfUsed + ? this.loadResourceWithCaching(this.cacheIfUsed, name, url, contentHash, resourceType) + : this.loadResourceWithoutCaching(name, url, contentHash, resourceType); + + return { name, url: toAbsoluteUri(url), response }; + } + + logToConsole(): void { + const cacheLoadsEntries = Object.values(this.cacheLoads); + const networkLoadsEntries = Object.values(this.networkLoads); + const cacheResponseBytes = countTotalBytes(cacheLoadsEntries); + const networkResponseBytes = countTotalBytes(networkLoadsEntries); + const totalResponseBytes = cacheResponseBytes + networkResponseBytes; + if (totalResponseBytes === 0) { + // We have no perf stats to display, likely because caching is not in use. + return; + } + + const linkerDisabledWarning = this.bootConfig.linkerEnabled ? "%c" : "\n%cThis application was built with linking (tree shaking) disabled. Published applications will be significantly smaller."; + console.groupCollapsed(`%cdotnet%c Loaded ${toDataSizeString(totalResponseBytes)} resources${linkerDisabledWarning}`, "background: purple; color: white; padding: 1px 3px; border-radius: 3px;", "font-weight: bold;", "font-weight: normal;"); + + if (cacheLoadsEntries.length) { + console.groupCollapsed(`Loaded ${toDataSizeString(cacheResponseBytes)} resources from cache`); + console.table(this.cacheLoads); + console.groupEnd(); + } + + if (networkLoadsEntries.length) { + console.groupCollapsed(`Loaded ${toDataSizeString(networkResponseBytes)} resources from network`); + console.table(this.networkLoads); + console.groupEnd(); + } + + console.groupEnd(); + } + + async purgeUnusedCacheEntriesAsync(): Promise { + // We want to keep the cache small because, even though the browser will evict entries if it + // gets too big, we don't want to be considered problematic by the end user viewing storage stats + const cache = this.cacheIfUsed; + if (cache) { + const cachedRequests = await cache.keys(); + const deletionPromises = cachedRequests.map(async cachedRequest => { + if (!(cachedRequest.url in this.usedCacheKeys)) { + await cache.delete(cachedRequest); + } + }); + + await Promise.all(deletionPromises); + } + } + + private async loadResourceWithCaching(cache: Cache, name: string, url: string, contentHash: string, resourceType: WebAssemblyBootResourceType) { + // Since we are going to cache the response, we require there to be a content hash for integrity + // checking. We don't want to cache bad responses. There should always be a hash, because the build + // process generates this data. + if (!contentHash || contentHash.length === 0) { + throw new Error("Content hash is required"); + } + + const cacheKey = toAbsoluteUri(`${url}.${contentHash}`); + this.usedCacheKeys[cacheKey] = true; + + let cachedResponse: Response | undefined; + try { + cachedResponse = await cache.match(cacheKey); + } catch { + // Be tolerant to errors reading from the cache. This is a guard for https://bugs.chromium.org/p/chromium/issues/detail?id=968444 where + // chromium browsers may sometimes throw when working with the cache. + } + + if (cachedResponse) { + // It's in the cache. + const responseBytes = parseInt(cachedResponse.headers.get("content-length") || "0"); + this.cacheLoads[name] = { responseBytes }; + return cachedResponse; + } else { + // It's not in the cache. Fetch from network. + const networkResponse = await this.loadResourceWithoutCaching(name, url, contentHash, resourceType); + this.addToCacheAsync(cache, name, cacheKey, networkResponse); // Don't await - add to cache in background + return networkResponse; + } + } + + private loadResourceWithoutCaching(name: string, url: string, contentHash: string, resourceType: WebAssemblyBootResourceType): Promise { + // Allow developers to override how the resource is loaded + if (this.startOptions.loadBootResource) { + const customLoadResult = this.startOptions.loadBootResource(resourceType, name, url, contentHash); + if (customLoadResult instanceof Promise) { + // They are supplying an entire custom response, so just use that + return customLoadResult; + } else if (typeof customLoadResult === "string") { + // They are supplying a custom URL, so use that with the default fetch behavior + url = customLoadResult; + } + } + + // Note that if cacheBootResources was explicitly disabled, we also bypass hash checking + // This is to give developers an easy opt-out from the entire caching/validation flow if + // there's anything they don't like about it. + return fetch(url, { + cache: networkFetchCacheMode, + integrity: this.bootConfig.cacheBootResources ? contentHash : undefined, + }); + } + + private async addToCacheAsync(cache: Cache, name: string, cacheKey: string, response: Response) { + // We have to clone in order to put this in the cache *and* not prevent other code from + // reading the original response stream. + const responseData = await response.clone().arrayBuffer(); + + // Now is an ideal moment to capture the performance stats for the request, since it + // only just completed and is most likely to still be in the buffer. However this is + // only done on a 'best effort' basis. Even if we do receive an entry, some of its + // properties may be blanked out if it was a CORS request. + const performanceEntry = getPerformanceEntry(response.url); + const responseBytes = (performanceEntry && performanceEntry.encodedBodySize) || undefined; + this.networkLoads[name] = { responseBytes }; + + // Add to cache as a custom response object so we can track extra data such as responseBytes + // We can't rely on the server sending content-length (ASP.NET Core doesn't by default) + const responseToCache = new Response(responseData, { + headers: { + "content-type": response.headers.get("content-type") || "", + "content-length": (responseBytes || response.headers.get("content-length") || "").toString(), + }, + }); + + try { + await cache.put(cacheKey, responseToCache); + } catch { + // Be tolerant to errors writing to the cache. This is a guard for https://bugs.chromium.org/p/chromium/issues/detail?id=968444 where + // chromium browsers may sometimes throw when performing cache operations. + } + } +} + +async function getCacheToUseIfEnabled(bootConfig: BootJsonData): Promise { + // caches will be undefined if we're running on an insecure origin (secure means https or localhost) + if (!bootConfig.cacheBootResources || typeof caches === "undefined") { + return null; + } + + // cache integrity is compromised if the first request has been served over http (except localhost) + // in this case, we want to disable caching and integrity validation + if (window.isSecureContext === false) { + return null; + } + + // Define a separate cache for each base href, so we're isolated from any other + // Blazor application running on the same origin. We need this so that we're free + // to purge from the cache anything we're not using and don't let it keep growing, + // since we don't want to be worst offenders for space usage. + const relativeBaseHref = document.baseURI.substring(document.location.origin.length); + const cacheName = `dotnet-resources-${relativeBaseHref}`; + + try { + // There's a Chromium bug we need to be aware of here: the CacheStorage APIs say that when + // caches.open(name) returns a promise that succeeds, the value is meant to be a Cache instance. + // However, if the browser was launched with a --user-data-dir param that's "too long" in some sense, + // then even through the promise resolves as success, the value given is `undefined`. + // See https://stackoverflow.com/a/46626574 and https://bugs.chromium.org/p/chromium/issues/detail?id=1054541 + // If we see this happening, return "null" to mean "proceed without caching". + return (await caches.open(cacheName)) || null; + } catch { + // There's no known scenario where we should get an exception here, but considering the + // Chromium bug above, let's tolerate it and treat as "proceed without caching". + return null; + } +} + +function countTotalBytes(loads: LoadLogEntry[]) { + return loads.reduce((prev, item) => prev + (item.responseBytes || 0), 0); +} + +function toDataSizeString(byteCount: number) { + return `${(byteCount / (1024 * 1024)).toFixed(2)} MB`; +} + +function getPerformanceEntry(url: string): PerformanceResourceTiming | undefined { + if (typeof performance !== "undefined") { + return performance.getEntriesByName(url)[0] as PerformanceResourceTiming; + } +} + +interface LoadLogEntry { + responseBytes: number | undefined; +} + +export interface LoadingResource { + name: string; + url: string; + response: Promise; +} diff --git a/src/mono/wasm/runtime/blazor/WebAssemblyStartOptions.ts b/src/mono/wasm/runtime/blazor/WebAssemblyStartOptions.ts new file mode 100644 index 00000000000000..3f8c0fc4b811fa --- /dev/null +++ b/src/mono/wasm/runtime/blazor/WebAssemblyStartOptions.ts @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +export interface WebAssemblyStartOptions { + /** + * Overrides the built-in boot resource loading mechanism so that boot resources can be fetched + * from a custom source, such as an external CDN. + * @param type The type of the resource to be loaded. + * @param name The name of the resource to be loaded. + * @param defaultUri The URI from which the framework would fetch the resource by default. The URI may be relative or absolute. + * @param integrity The integrity string representing the expected content in the response. + * @returns A URI string or a Response promise to override the loading process, or null/undefined to allow the default loading behavior. + */ + loadBootResource(type: WebAssemblyBootResourceType, name: string, defaultUri: string, integrity: string): string | Promise | null | undefined; + + /** + * Override built-in environment setting on start. + */ + environment?: string; + + /** + * Gets the application culture. This is a name specified in the BCP 47 format. See https://tools.ietf.org/html/bcp47 + */ + applicationCulture?: string; +} + +// This type doesn't have to align with anything in BootConfig. +// Instead, this represents the public API through which certain aspects +// of boot resource loading can be customized. +export type WebAssemblyBootResourceType = "assembly" | "pdb" | "dotnetjs" | "dotnetwasm" | "globalization" | "manifest" | "configuration"; diff --git a/src/mono/wasm/runtime/blazor/_Integration.ts b/src/mono/wasm/runtime/blazor/_Integration.ts new file mode 100644 index 00000000000000..7518796505091c --- /dev/null +++ b/src/mono/wasm/runtime/blazor/_Integration.ts @@ -0,0 +1,220 @@ +import { INTERNAL, Module } from "../imports"; +import { AssetEntry, LoadingResource, MonoConfigInternal } from "../types"; +import { BootConfigResult, BootJsonData, ICUDataMode } from "./BootConfig"; +import { WebAssemblyResourceLoader } from "./WebAssemblyResourceLoader"; +import { WebAssemblyBootResourceType } from "./WebAssemblyStartOptions"; +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; + + const resourceLoader = await WebAssemblyResourceLoader.initAsync(bootConfigResult.bootConfig, candidateOptions || {}); + + INTERNAL.resourceLoader = resourceLoader; + + const newConfig = mapBootConfigToMonoConfig(Module.config as MonoConfigInternal, resourceLoader, bootConfigResult.applicationEnvironment); + Module.config = newConfig; +} + +let resourcesLoaded = 0; +let totalResources = 0; + +export 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"; +} \ No newline at end of file diff --git a/src/mono/wasm/runtime/blazor/_Polyfill.ts b/src/mono/wasm/runtime/blazor/_Polyfill.ts new file mode 100644 index 00000000000000..dc4aded75cc921 --- /dev/null +++ b/src/mono/wasm/runtime/blazor/_Polyfill.ts @@ -0,0 +1,38 @@ +import { BootJsonData } from "./BootConfig"; + +let testAnchor: HTMLAnchorElement; +export function toAbsoluteUri(relativeUri: string): string { + testAnchor = testAnchor || document.createElement("a"); + testAnchor.href = relativeUri; + return testAnchor.href; +} + +export function hasDebuggingEnabled(bootConfig: BootJsonData): boolean { + // Copied from blazor MonoDebugger.ts/attachDebuggerHotkey + + const hasReferencedPdbs = !!bootConfig.resources.pdb; + const debugBuild = bootConfig.debugBuild; + + const navigatorUA = navigator as MonoNavigatorUserAgent; + const brands = navigatorUA.userAgentData && navigatorUA.userAgentData.brands; + const currentBrowserIsChromeOrEdge = brands + ? brands.some(b => b.brand === "Google Chrome" || b.brand === "Microsoft Edge" || b.brand === "Chromium") + : (window as any).chrome; + + return (hasReferencedPdbs || debugBuild) && (currentBrowserIsChromeOrEdge || navigator.userAgent.includes("Firefox")); +} + +// can be removed once userAgentData is part of lib.dom.d.ts +declare interface MonoNavigatorUserAgent extends Navigator { + readonly userAgentData: MonoUserAgentData; +} + +declare interface MonoUserAgentData { + readonly brands: ReadonlyArray; + readonly platform: string; +} + +declare interface MonoUserAgentDataBrandVersion { + brand?: string; + version?: string; +} \ No newline at end of file diff --git a/src/mono/wasm/runtime/dotnet.d.ts b/src/mono/wasm/runtime/dotnet.d.ts index 4a678cf7eb21c2..111491048da668 100644 --- a/src/mono/wasm/runtime/dotnet.d.ts +++ b/src/mono/wasm/runtime/dotnet.d.ts @@ -5,6 +5,51 @@ //! This is not considered public API with backward compatibility guarantees. +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; + modifiableAssemblies: string | null; + aspnetCoreBrowserTools: string | null; +} +type BootJsonDataExtension = { + [extensionName: string]: ResourceList; +}; +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; +} +type ResourceList = { + [name: string]: string; +}; +type ExtendedResourceList = { + [name: string]: { + hash: string; + behavior: string; + }; +}; +declare enum ICUDataMode { + Sharded = 0, + All = 1, + Invariant = 2, + Custom = 3 +} + interface DotnetHostBuilder { withConfig(config: MonoConfig): DotnetHostBuilder; withConfigSrc(configSrc: string): DotnetHostBuilder; @@ -137,6 +182,10 @@ type MonoConfig = { * hash of assets */ assetsHash?: string; + /** + * application environment + */ + applicationEnvironment?: string; }; interface ResourceRequest { name: string; @@ -188,6 +237,8 @@ type DotnetModuleConfig = { configSrc?: string; onConfigLoaded?: (config: MonoConfig) => void | Promise; onDotnetReady?: () => void | Promise; + onDownloadResourceProgress?: (resourcesLoaded: number, totalResources: number) => void; + getApplicationEnvironment?: (bootConfigResponse: Response) => string | null; imports?: any; exports?: string[]; downloadResource?: (request: ResourceRequest) => LoadingResource | undefined; @@ -280,4 +331,4 @@ declare global { declare const dotnet: ModuleAPI["dotnet"]; declare const exit: ModuleAPI["exit"]; -export { AssetEntry, CreateDotnetRuntimeType, DotnetModuleConfig, EmscriptenModule, IMemoryView, ModuleAPI, MonoConfig, ResourceRequest, RuntimeAPI, createDotnetRuntime as default, dotnet, exit }; +export { AssetEntry, BootJsonData, CreateDotnetRuntimeType, DotnetModuleConfig, EmscriptenModule, ICUDataMode, IMemoryView, ModuleAPI, MonoConfig, ResourceRequest, RuntimeAPI, createDotnetRuntime as default, dotnet, exit }; diff --git a/src/mono/wasm/runtime/export-types.ts b/src/mono/wasm/runtime/export-types.ts index fecc9eaa318c0d..1221d3f2fc0bba 100644 --- a/src/mono/wasm/runtime/export-types.ts +++ b/src/mono/wasm/runtime/export-types.ts @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +import { BootJsonData, ICUDataMode } from "./blazor/BootConfig"; import { IMemoryView } from "./marshal"; import { createDotnetRuntime, CreateDotnetRuntimeType, DotnetModuleConfig, RuntimeAPI, MonoConfig, ModuleAPI, AssetEntry, ResourceRequest } from "./types"; import { EmscriptenModule } from "./types/emscripten"; @@ -22,6 +23,6 @@ declare const exit: ModuleAPI["exit"]; export { EmscriptenModule, - RuntimeAPI, ModuleAPI, DotnetModuleConfig, CreateDotnetRuntimeType, MonoConfig, IMemoryView, AssetEntry, ResourceRequest, + RuntimeAPI, ModuleAPI, DotnetModuleConfig, CreateDotnetRuntimeType, MonoConfig, IMemoryView, AssetEntry, ResourceRequest, BootJsonData, ICUDataMode, dotnet, exit }; diff --git a/src/mono/wasm/runtime/run-outer.ts b/src/mono/wasm/runtime/run-outer.ts index b092418135d7aa..8785c0e2c639d2 100644 --- a/src/mono/wasm/runtime/run-outer.ts +++ b/src/mono/wasm/runtime/run-outer.ts @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // WARNING: code in this file is executed before any of the emscripten code, so there is very little initialized already +import { WebAssemblyStartOptions } from "./blazor/WebAssemblyStartOptions"; import { emscriptenEntrypoint, runtimeHelpers } from "./imports"; import { setup_proxy_console } from "./logging"; import { mono_exit } from "./run"; @@ -244,7 +245,7 @@ class HostBuilder implements DotnetHostBuilder { try { mono_assert(runtimeOptions && Array.isArray(runtimeOptions), "must be array of strings"); const configInternal = this.moduleConfig.config as MonoConfigInternal; - configInternal.runtimeOptions = [...(configInternal.runtimeOptions || []), ...(runtimeOptions|| [])]; + configInternal.runtimeOptions = [...(configInternal.runtimeOptions || []), ...(runtimeOptions || [])]; return this; } catch (err) { mono_exit(1, err); @@ -281,6 +282,12 @@ class HostBuilder implements DotnetHostBuilder { } } + withStartupOptions(startupOptions: Partial): DotnetHostBuilder { + const configInternal = this.moduleConfig.config as MonoConfigInternal; + configInternal.startupOptions = startupOptions; + return this.withConfigSrc("blazor.boot.json"); + } + async create(): Promise { try { if (!this.instance) { diff --git a/src/mono/wasm/runtime/startup.ts b/src/mono/wasm/runtime/startup.ts index d1a76cf7f5a78a..675c62c03d65a2 100644 --- a/src/mono/wasm/runtime/startup.ts +++ b/src/mono/wasm/runtime/startup.ts @@ -26,6 +26,7 @@ import { preAllocatePThreadWorkerPool, instantiateWasmPThreadWorkerPool } from " import { export_linker } from "./exports-linker"; import { endMeasure, MeasuredBlock, startMeasure } from "./profiler"; import { getMemorySnapshot, storeMemorySnapshot, getMemorySnapshotSize } from "./snapshot"; +import { loadBootConfig } from "./blazor/_Integration"; // legacy import { init_legacy_exports } from "./net6-legacy/corebindings"; @@ -272,6 +273,13 @@ async function onRuntimeInitializedAsync(userOnRuntimeInitialized: () => void) { string_decoder.init_fields(); }); + if (config.startupOptions && INTERNAL.resourceLoader) { + if (INTERNAL.resourceLoader.bootConfig.debugBuild && INTERNAL.resourceLoader.bootConfig.cacheBootResources) { + INTERNAL.resourceLoader.logToConsole(); + } + INTERNAL.resourceLoader.purgeUnusedCacheEntriesAsync(); // Don't await - it's fine to run in background + } + // call user code try { userOnRuntimeInitialized(); @@ -639,17 +647,21 @@ export async function mono_wasm_load_config(configFilePath?: string): Promise }; export type RunArguments = { @@ -268,6 +275,8 @@ export type DotnetModuleConfig = { configSrc?: string, onConfigLoaded?: (config: MonoConfig) => void | Promise; onDotnetReady?: () => void | Promise; + onDownloadResourceProgress?: (resourcesLoaded: number, totalResources: number) => void; + getApplicationEnvironment?: (bootConfigResponse: Response) => string | null; imports?: any; exports?: string[];