From 7a9ff69fd75e717f69e24e8ba7c6762a6c2006e4 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Marek=20Fi=C5=A1era?= Date: Sat, 29 Jul 2023 18:45:05 +0200 Subject: [PATCH] [browser] Clean-up typescript code imported from Blazor (#89435) --- .../Microsoft.NET.Sdk.WebAssembly.Browser.targets | 8 +- .../wasm/Wasm.Build.Tests/ProjectProviderBase.cs | 10 +- src/mono/wasm/runtime/dotnet.d.ts | 101 ++++--- src/mono/wasm/runtime/lazyLoading.ts | 34 ++- src/mono/wasm/runtime/loader/assets.ts | 311 +++++++++++++++++--- src/mono/wasm/runtime/loader/assetsCache.ts | 201 +++++++++++++ src/mono/wasm/runtime/loader/blazor/BootConfig.ts | 54 ---- .../loader/blazor/WebAssemblyResourceLoader.ts | 248 ---------------- .../wasm/runtime/loader/blazor/_Integration.ts | 314 --------------------- src/mono/wasm/runtime/loader/blazor/_Polyfill.ts | 22 -- src/mono/wasm/runtime/loader/config.ts | 175 ++++++++++-- src/mono/wasm/runtime/loader/globals.ts | 10 +- src/mono/wasm/runtime/loader/icu.ts | 62 ++-- .../wasm/runtime/loader/libraryInitializers.ts | 10 +- src/mono/wasm/runtime/loader/polyfills.ts | 3 +- src/mono/wasm/runtime/loader/run.ts | 24 +- src/mono/wasm/runtime/satelliteAssemblies.ts | 31 +- src/mono/wasm/runtime/snapshot.ts | 16 +- src/mono/wasm/runtime/startup.ts | 8 +- src/mono/wasm/runtime/types/blazor.ts | 58 ---- src/mono/wasm/runtime/types/index.ts | 134 ++++----- src/mono/wasm/runtime/types/internal.ts | 15 +- .../BootJsonBuilderHelper.cs | 77 +++++ .../BootJsonData.cs | 75 +++-- .../GenerateWasmBootJson.cs | 125 +++++--- ...Microsoft.NET.Sdk.WebAssembly.Pack.Tasks.csproj | 1 + src/tasks/WasmAppBuilder/WasmAppBuilder.cs | 74 ++--- src/tasks/WasmAppBuilder/WasmAppBuilder.csproj | 1 + 28 files changed, 1084 insertions(+), 1118 deletions(-) create mode 100644 src/mono/wasm/runtime/loader/assetsCache.ts delete mode 100644 src/mono/wasm/runtime/loader/blazor/BootConfig.ts delete mode 100644 src/mono/wasm/runtime/loader/blazor/WebAssemblyResourceLoader.ts delete mode 100644 src/mono/wasm/runtime/loader/blazor/_Integration.ts delete mode 100644 src/mono/wasm/runtime/loader/blazor/_Polyfill.ts delete mode 100644 src/mono/wasm/runtime/types/blazor.ts create mode 100644 src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/BootJsonBuilderHelper.cs diff --git a/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.targets b/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.targets index be765da..593825f 100644 --- a/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.targets +++ b/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.targets @@ -372,8 +372,8 @@ Copyright (c) .NET Foundation. All rights reserved. RuntimeOptions="$(_BlazorWebAssemblyRuntimeOptions)" Extensions="@(WasmBootConfigExtension)" TargetFrameworkVersion="$(TargetFrameworkVersion)" - LibraryInitializerOnRuntimeConfigLoaded="@(WasmLibraryInitializerOnRuntimeConfigLoaded)" - LibraryInitializerOnRuntimeReady="@(WasmLibraryInitializerOnRuntimeReady)" /> + ModuleAfterConfigLoaded="@(WasmModuleAfterConfigLoaded)" + ModuleAfterRuntimeReady="@(WasmModuleAfterRuntimeReady)" /> @@ -563,8 +563,8 @@ Copyright (c) .NET Foundation. All rights reserved. RuntimeOptions="$(_BlazorWebAssemblyRuntimeOptions)" Extensions="@(WasmBootConfigExtension)" TargetFrameworkVersion="$(TargetFrameworkVersion)" - LibraryInitializerOnRuntimeConfigLoaded="@(WasmLibraryInitializerOnRuntimeConfigLoaded)" - LibraryInitializerOnRuntimeReady="@(WasmLibraryInitializerOnRuntimeReady)" /> + ModuleAfterConfigLoaded="@(WasmModuleAfterConfigLoaded)" + ModuleAfterRuntimeReady="@(WasmModuleAfterRuntimeReady)" /> diff --git a/src/mono/wasm/Wasm.Build.Tests/ProjectProviderBase.cs b/src/mono/wasm/Wasm.Build.Tests/ProjectProviderBase.cs index ccdf07d..99d624b 100644 --- a/src/mono/wasm/Wasm.Build.Tests/ProjectProviderBase.cs +++ b/src/mono/wasm/Wasm.Build.Tests/ProjectProviderBase.cs @@ -390,7 +390,12 @@ public abstract class ProjectProviderBase(ITestOutputHelper _testOutput, string? Assert.True(File.Exists(bootJsonPath), $"Expected to find {bootJsonPath}"); BootJsonData bootJson = ParseBootData(bootJsonPath); - var bootJsonEntries = bootJson.resources.runtime.Keys.Where(k => k.StartsWith("dotnet.", StringComparison.Ordinal)).ToArray(); + var bootJsonEntries = bootJson.resources.jsModuleNative.Keys + .Union(bootJson.resources.jsModuleRuntime.Keys) + .Union(bootJson.resources.jsModuleWorker?.Keys ?? Enumerable.Empty()) + .Union(bootJson.resources.jsSymbols?.Keys ?? Enumerable.Empty()) + .Union(bootJson.resources.wasmNative.Keys) + .ToArray(); var expectedEntries = new SortedDictionary>(); IReadOnlySet expected = GetDotNetFilesExpectedSet(options); @@ -398,7 +403,8 @@ public abstract class ProjectProviderBase(ITestOutputHelper _testOutput, string? var knownSet = GetAllKnownDotnetFilesToFingerprintMap(options); foreach (string expectedFilename in expected) { - if (Path.GetExtension(expectedFilename) == ".map") + // FIXME: Find a systematic solution for skipping dotnet.js from boot json check + if (expectedFilename == "dotnet.js" || Path.GetExtension(expectedFilename) == ".map") continue; bool expectFingerprint = knownSet[expectedFilename]; diff --git a/src/mono/wasm/runtime/dotnet.d.ts b/src/mono/wasm/runtime/dotnet.d.ts index e42d6e1..3bb25af 100644 --- a/src/mono/wasm/runtime/dotnet.d.ts +++ b/src/mono/wasm/runtime/dotnet.d.ts @@ -96,14 +96,6 @@ interface DotnetHostBuilder { } type MonoConfig = { /** - * The subfolder containing managed assemblies and pdbs. This is relative to dotnet.js script. - */ - assemblyRootFolder?: string; - /** - * A list of assets to load along with the runtime. - */ - assets?: AssetEntry[]; - /** * Additional search locations for assets. */ remoteSources?: string[]; @@ -134,6 +126,10 @@ type MonoConfig = { */ debugLevel?: number; /** + * Gets a value that determines whether to enable caching of the 'resources' inside a CacheStorage instance within the browser. + */ + cacheBootResources?: boolean; + /** * Enables diagnostic log messages during startup */ diagnosticTracing?: boolean; @@ -152,10 +148,6 @@ type MonoConfig = { */ startupMemoryCache?: boolean; /** - * hash of assets - */ - assetsHash?: string; - /** * application environment */ applicationEnvironment?: string; @@ -168,6 +160,10 @@ type MonoConfig = { */ resources?: ResourceGroups; /** + * appsettings files to load to VFS + */ + appsettings?: string[]; + /** * config extensions declared in MSBuild items @(WasmBootConfigExtension) */ extensions?: { @@ -178,26 +174,31 @@ type ResourceExtensions = { [extensionName: string]: ResourceList; }; interface ResourceGroups { - readonly hash?: string; - readonly assembly?: ResourceList; - readonly lazyAssembly?: ResourceList; - readonly pdb?: ResourceList; - readonly runtime?: ResourceList; - readonly satelliteResources?: { + hash?: string; + assembly?: ResourceList; + lazyAssembly?: ResourceList; + pdb?: ResourceList; + jsModuleWorker?: ResourceList; + jsModuleNative: ResourceList; + jsModuleRuntime: ResourceList; + jsSymbols?: ResourceList; + wasmNative: ResourceList; + icu?: ResourceList; + satelliteResources?: { [cultureName: string]: ResourceList; }; - readonly libraryInitializers?: ResourceList; - readonly libraryStartupModules?: { - readonly onRuntimeConfigLoaded?: ResourceList; - readonly onRuntimeReady?: ResourceList; - }; - readonly extensions?: ResourceExtensions; - readonly vfs?: { + modulesAfterConfigLoaded?: ResourceList; + modulesAfterRuntimeReady?: ResourceList; + extensions?: ResourceExtensions; + vfs?: { [virtualPath: string]: ResourceList; }; } +/** + * A "key" is name of the file, a "value" is optional hash for integrity check. + */ type ResourceList = { - [name: string]: string; + [name: string]: string | null | ""; }; /** * Overrides the built-in boot resource loading mechanism so that boot resources can be fetched @@ -208,12 +209,12 @@ type ResourceList = { * @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. */ -type LoadBootResourceCallback = (type: WebAssemblyBootResourceType, name: string, defaultUri: string, integrity: string) => string | Promise | null | undefined; +type LoadBootResourceCallback = (type: AssetBehaviors | "manifest", name: string, defaultUri: string, integrity: string) => string | Promise | null | undefined; interface ResourceRequest { name: string; - behavior: AssetBehaviours; + behavior: AssetBehaviors; resolvedUrl?: string; - hash?: string; + hash?: string | null | ""; } interface LoadingResource { name: string; @@ -248,7 +249,24 @@ interface AssetEntry extends ResourceRequest { */ pendingDownload?: LoadingResource; } -type AssetBehaviours = +type SingleAssetBehaviors = +/** + * The binary of the dotnet runtime. + */ +"dotnetwasm" +/** + * The javascript module for threads. + */ + | "js-module-threads" +/** + * The javascript module for threads. + */ + | "js-module-runtime" +/** + * The javascript module for threads. + */ + | "js-module-native"; +type AssetBehaviors = SingleAssetBehaviors | /** * Load asset as a managed resource assembly. */ @@ -274,26 +292,6 @@ type AssetBehaviours = */ | "vfs" /** - * The binary of the dotnet runtime. - */ - | "dotnetwasm" -/** - * The javascript module for threads. - */ - | "js-module-threads" -/** - * The javascript module for threads. - */ - | "js-module-runtime" -/** - * The javascript module for threads. - */ - | "js-module-dotnet" -/** - * The javascript module for threads. - */ - | "js-module-native" -/** * The javascript module that came from nuget package . */ | "js-module-library-initializer" @@ -330,10 +328,8 @@ type DotnetModuleConfig = { 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; } & Partial; type APIType = { runMain: (mainAssemblyName: string, args: string[]) => Promise; @@ -400,7 +396,6 @@ type ModuleAPI = { exit: (code: number, reason?: any) => void; }; type CreateDotnetRuntimeType = (moduleFactory: DotnetModuleConfig | ((api: RuntimeAPI) => DotnetModuleConfig)) => Promise; -type WebAssemblyBootResourceType = "assembly" | "pdb" | "dotnetjs" | "dotnetwasm" | "globalization" | "manifest" | "configuration"; interface IDisposable { dispose(): void; diff --git a/src/mono/wasm/runtime/lazyLoading.ts b/src/mono/wasm/runtime/lazyLoading.ts index 8402e7f..59c153a 100644 --- a/src/mono/wasm/runtime/lazyLoading.ts +++ b/src/mono/wasm/runtime/lazyLoading.ts @@ -1,40 +1,50 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -import { INTERNAL, loaderHelpers, runtimeHelpers } from "./globals"; -import type { WebAssemblyResourceLoader } from "./loader/blazor/WebAssemblyResourceLoader"; +import { loaderHelpers, runtimeHelpers } from "./globals"; +import { AssetEntry } from "./types"; export async function loadLazyAssembly(assemblyNameToLoad: string): Promise { - const resourceLoader: WebAssemblyResourceLoader = INTERNAL.resourceLoader; - const resources = resourceLoader.bootConfig.resources; + const resources = loaderHelpers.config.resources!; const lazyAssemblies = resources.lazyAssembly; if (!lazyAssemblies) { throw new Error("No assemblies have been marked as lazy-loadable. Use the 'BlazorWebAssemblyLazyLoad' item group in your project file to enable lazy loading an assembly."); } - const assemblyMarkedAsLazy = Object.prototype.hasOwnProperty.call(lazyAssemblies, assemblyNameToLoad); - if (!assemblyMarkedAsLazy) { + if (!lazyAssemblies[assemblyNameToLoad]) { throw new Error(`${assemblyNameToLoad} must be marked with 'BlazorWebAssemblyLazyLoad' item group in your project file to allow lazy-loading.`); } + const dllAsset: AssetEntry = { + name: assemblyNameToLoad, + hash: lazyAssemblies[assemblyNameToLoad], + behavior: "assembly", + }; + if (loaderHelpers.loadedAssemblies.some(f => f.includes(assemblyNameToLoad))) { return false; } - const dllNameToLoad = assemblyNameToLoad; - const pdbNameToLoad = changeExtension(assemblyNameToLoad, ".pdb"); - const shouldLoadPdb = loaderHelpers.hasDebuggingEnabled(resourceLoader.bootConfig) && resources.pdb && Object.prototype.hasOwnProperty.call(lazyAssemblies, pdbNameToLoad); + const pdbNameToLoad = changeExtension(dllAsset.name, ".pdb"); + const shouldLoadPdb = loaderHelpers.hasDebuggingEnabled(loaderHelpers.config) && Object.prototype.hasOwnProperty.call(lazyAssemblies, pdbNameToLoad); - const dllBytesPromise = resourceLoader.loadResource(dllNameToLoad, loaderHelpers.locateFile(dllNameToLoad), lazyAssemblies[dllNameToLoad], "assembly").response.then(response => response.arrayBuffer()); + const dllBytesPromise = loaderHelpers.retrieve_asset_download(dllAsset); let dll = null; let pdb = null; if (shouldLoadPdb) { - const pdbBytesPromise = await resourceLoader.loadResource(pdbNameToLoad, loaderHelpers.locateFile(pdbNameToLoad), lazyAssemblies[pdbNameToLoad], "pdb").response.then(response => response.arrayBuffer()); + const pdbBytesPromise = lazyAssemblies[pdbNameToLoad] + ? loaderHelpers.retrieve_asset_download({ + name: pdbNameToLoad, + hash: lazyAssemblies[pdbNameToLoad], + behavior: "pdb" + }) + : Promise.resolve(null); + const [dllBytes, pdbBytes] = await Promise.all([dllBytesPromise, pdbBytesPromise]); dll = new Uint8Array(dllBytes); - pdb = new Uint8Array(pdbBytes); + pdb = pdbBytes ? new Uint8Array(pdbBytes) : null; } else { const dllBytes = await dllBytesPromise; dll = new Uint8Array(dllBytes); diff --git a/src/mono/wasm/runtime/loader/assets.ts b/src/mono/wasm/runtime/loader/assets.ts index 5cf6ec9..92c944e 100644 --- a/src/mono/wasm/runtime/loader/assets.ts +++ b/src/mono/wasm/runtime/loader/assets.ts @@ -2,11 +2,13 @@ // The .NET Foundation licenses this file to you under the MIT license. import type { AssetEntryInternal, PromiseAndController } from "../types/internal"; -import type { AssetBehaviours, AssetEntry, LoadingResource, ResourceRequest } from "../types"; +import type { AssetBehaviors, AssetEntry, LoadingResource, ResourceList, ResourceRequest, SingleAssetBehaviors as SingleAssetBehaviors, WebAssemblyBootResourceType } from "../types"; import { ENVIRONMENT_IS_NODE, ENVIRONMENT_IS_SHELL, loaderHelpers, mono_assert, runtimeHelpers } from "./globals"; import { createPromiseController } from "./promise-controller"; import { mono_log_debug } from "./logging"; import { mono_exit } from "./exit"; +import { addCachedReponse, findCachedResponse, isCacheAvailable } from "./assetsCache"; +import { getIcuResourceName } from "./icu"; let throttlingPromise: PromiseAndController | undefined; @@ -19,7 +21,6 @@ const jsModulesAssetTypes: { "js-module-threads": true, "js-module-runtime": true, "js-module-native": true, - "js-module-dotnet": true, }; // don't `fetch` javaScript and wasm files @@ -63,14 +64,47 @@ export function shouldLoadIcuAsset(asset: AssetEntryInternal): boolean { return !(asset.behavior == "icu" && asset.name != loaderHelpers.preferredIcuAsset); } -export function resolve_asset_path(behavior: AssetBehaviours): AssetEntryInternal { - const asset: AssetEntryInternal | undefined = loaderHelpers.config.assets?.find(a => a.behavior == behavior); - mono_assert(asset, () => `Can't find asset for ${behavior}`); - if (!asset.resolvedUrl) { - asset.resolvedUrl = resolve_path(asset, ""); +function getSingleAssetWithResolvedUrl(resources: ResourceList | undefined, behavior: SingleAssetBehaviors): AssetEntry { + const keys = Object.keys(resources || {}); + mono_assert(keys.length == 1, `Expect to have one ${behavior} asset in resources`); + + const name = keys[0]; + const asset = { + name, + hash: resources![name], + behavior, + resolvedUrl: appendUniqueQuery(loaderHelpers.locateFile(name), behavior) + }; + + const customSrc = invokeLoadBootResource(asset); + if (typeof (customSrc) === "string") { + asset.resolvedUrl = customSrc; + } else if (customSrc) { + // Since we must load this via a import, it's only valid to supply a URI (and not a Request, say) + throw new Error(`For a ${behavior} resource, custom loaders must supply a URI string.`); } + return asset; } + +export function resolve_single_asset_path(behavior: SingleAssetBehaviors): AssetEntryInternal { + const resources = loaderHelpers.config.resources; + mono_assert(resources, "Can't find resources in config"); + + switch (behavior) { + case "dotnetwasm": + return getSingleAssetWithResolvedUrl(resources.wasmNative, behavior); + case "js-module-threads": + return getSingleAssetWithResolvedUrl(resources.jsModuleWorker, behavior); + case "js-module-native": + return getSingleAssetWithResolvedUrl(resources.jsModuleNative, behavior); + case "js-module-runtime": + return getSingleAssetWithResolvedUrl(resources.jsModuleRuntime, behavior); + default: + throw new Error(`Unknown single asset behavior ${behavior}`); + } +} + export async function mono_download_assets(): Promise { mono_log_debug("mono_download_assets"); loaderHelpers.maxParallelDownloads = loaderHelpers.config.maxParallelDownloads || loaderHelpers.maxParallelDownloads; @@ -80,20 +114,7 @@ export async function mono_download_assets(): Promise { const containedInSnapshotAssets: AssetEntryInternal[] = []; const promises_of_assets: Promise[] = []; - for (const a of loaderHelpers.config.assets!) { - const asset: AssetEntryInternal = a; - mono_assert(typeof asset === "object", "asset must be object"); - mono_assert(typeof asset.behavior === "string", "asset behavior must be known string"); - mono_assert(typeof asset.name === "string", "asset name must be string"); - mono_assert(!asset.resolvedUrl || typeof asset.resolvedUrl === "string", "asset resolvedUrl could be string"); - mono_assert(!asset.hash || typeof asset.hash === "string", "asset resolvedUrl could be string"); - mono_assert(!asset.pendingDownload || typeof asset.pendingDownload === "object", "asset pendingDownload could be object"); - if (containedInSnapshotByAssetTypes[asset.behavior]) { - containedInSnapshotAssets.push(asset); - } else { - alwaysLoadedAssets.push(asset); - } - } + prepareAssets(containedInSnapshotAssets, alwaysLoadedAssets); const countAndStartDownload = (asset: AssetEntryInternal) => { if (!skipInstantiateByAssetTypes[asset.behavior] && shouldLoadIcuAsset(asset)) { @@ -197,10 +218,132 @@ export async function mono_download_assets(): Promise { } } +function prepareAssets(containedInSnapshotAssets: AssetEntryInternal[], alwaysLoadedAssets: AssetEntryInternal[]) { + const config = loaderHelpers.config; + const resources = loaderHelpers.config.resources; + if (resources) { + if (resources.assembly) { + for (const name in resources.assembly) { + containedInSnapshotAssets.push({ + name, + hash: resources.assembly[name], + behavior: "assembly" + }); + } + } + + if (config.debugLevel != 0 && resources.pdb) { + for (const name in resources.pdb) { + containedInSnapshotAssets.push({ + name, + hash: resources.pdb[name], + behavior: "pdb" + }); + } + } + + if (config.loadAllSatelliteResources && resources.satelliteResources) { + for (const culture in resources.satelliteResources) { + for (const name in resources.satelliteResources[culture]) { + containedInSnapshotAssets.push({ + name, + hash: resources.satelliteResources[culture][name], + behavior: "resource", + culture + }); + } + } + } + + if (resources.vfs) { + for (const virtualPath in resources.vfs) { + for (const name in resources.vfs[virtualPath]) { + alwaysLoadedAssets.push({ + name, + hash: resources.vfs[virtualPath][name], + behavior: "vfs", + virtualPath + }); + } + } + } + + const icuDataResourceName = getIcuResourceName(config); + if (icuDataResourceName && resources.icu) { + for (const name in resources.icu) { + if (name === icuDataResourceName) { + containedInSnapshotAssets.push({ + name, + hash: resources.icu[name], + behavior: "icu", + loadRemote: true + }); + } + } + } + + if (resources.jsSymbols) { + for (const name in resources.jsSymbols) { + alwaysLoadedAssets.push({ + name, + hash: resources.jsSymbols[name], + behavior: "symbols" + }); + } + } + } + + if (config.appsettings) { + for (let i = 0; i < config.appsettings.length; i++) { + const configUrl = config.appsettings[i]; + const configFileName = fileName(configUrl); + if (configFileName === "appsettings.json" || configFileName === `appsettings.${config.applicationEnvironment}.json`) { + alwaysLoadedAssets.push({ + name: configFileName, + resolvedUrl: appendUniqueQuery(loaderHelpers.locateFile(configUrl), "vfs"), + behavior: "vfs" + }); + } + } + } + + const newAssets = [...containedInSnapshotAssets, ...alwaysLoadedAssets]; + + if (loaderHelpers.config.assets) { + for (const a of loaderHelpers.config.assets) { + const asset: AssetEntryInternal = a; + mono_assert(typeof asset === "object", "asset must be object"); + mono_assert(typeof asset.behavior === "string", "asset behavior must be known string"); + mono_assert(typeof asset.name === "string", "asset name must be string"); + mono_assert(!asset.resolvedUrl || typeof asset.resolvedUrl === "string", "asset resolvedUrl could be string"); + mono_assert(!asset.hash || typeof asset.hash === "string", "asset resolvedUrl could be string"); + mono_assert(!asset.pendingDownload || typeof asset.pendingDownload === "object", "asset pendingDownload could be object"); + if (containedInSnapshotByAssetTypes[asset.behavior]) { + containedInSnapshotAssets.push(asset); + } else { + alwaysLoadedAssets.push(asset); + } + } + } + + if (!loaderHelpers.config.assets) { + loaderHelpers.config.assets = []; + } + + loaderHelpers.config.assets = [...loaderHelpers.config.assets, ...newAssets]; + +} + export function delay(ms: number): Promise { return new Promise(resolve => globalThis.setTimeout(resolve, ms)); } +export async function retrieve_asset_download(asset: AssetEntry): Promise { + const pendingAsset = await start_asset_download(asset); + await pendingAsset.pendingDownloadInternal!.response; + return pendingAsset.buffer!; +} + // FIXME: Connection reset is probably the only good one for which we should retry export async function start_asset_download(asset: AssetEntryInternal): Promise { try { @@ -353,19 +496,14 @@ async function start_asset_download_sources(asset: AssetEntryInternal): Promise< function resolve_path(asset: AssetEntry, sourcePrefix: string): string { mono_assert(sourcePrefix !== null && sourcePrefix !== undefined, () => `sourcePrefix must be provided for ${asset.name}`); let attemptUrl; - const assemblyRootFolder = loaderHelpers.config.assemblyRootFolder; if (!asset.resolvedUrl) { if (sourcePrefix === "") { if (asset.behavior === "assembly" || asset.behavior === "pdb") { - attemptUrl = assemblyRootFolder - ? (assemblyRootFolder + "/" + asset.name) - : asset.name; + attemptUrl = asset.name; } else if (asset.behavior === "resource") { const path = asset.culture && asset.culture !== "" ? `${asset.culture}/${asset.name}` : asset.name; - attemptUrl = assemblyRootFolder - ? (assemblyRootFolder + "/" + path) - : path; + attemptUrl = path; } else { attemptUrl = asset.name; @@ -382,7 +520,7 @@ function resolve_path(asset: AssetEntry, sourcePrefix: string): string { return attemptUrl; } -export function appendUniqueQuery(attemptUrl: string, behavior: AssetBehaviours): string { +export function appendUniqueQuery(attemptUrl: string, behavior: AssetBehaviors): string { // apply unique query to js modules to make the module state independent of the other runtime instances if (loaderHelpers.modulesUniqueQuery && jsModulesAssetTypes[behavior]) { attemptUrl = attemptUrl + loaderHelpers.modulesUniqueQuery; @@ -391,22 +529,26 @@ export function appendUniqueQuery(attemptUrl: string, behavior: AssetBehaviours) return attemptUrl; } - +let resourcesLoaded = 0; +const totalResources = new Set(); function download_resource(request: ResourceRequest): LoadingResource { try { - if (typeof loaderHelpers.downloadResource === "function") { - const loading = loaderHelpers.downloadResource(request); - if (loading) return loading; - } - const options: any = {}; - if (request.hash) { - options.integrity = request.hash; - } - const response = loaderHelpers.fetch_like(request.resolvedUrl!, options); - return { - name: request.name, url: request.resolvedUrl!, response - }; + mono_assert(request.resolvedUrl, "Request's resolvedUrl must be set"); + const fetchResponse = download_resource_with_cache(request); + const response = { name: request.name, url: request.resolvedUrl, response: fetchResponse }; + + totalResources.add(request.name!); + response.response.then(() => { + if (request.behavior == "assembly") { + loaderHelpers.loadedAssemblies.push(request.resolvedUrl!); + } + + resourcesLoaded++; + if (loaderHelpers.onDownloadResourceProgress) + loaderHelpers.onDownloadResourceProgress(resourcesLoaded, totalResources.size); + }); + return response; } catch (err) { const response = { ok: false, @@ -422,9 +564,94 @@ function download_resource(request: ResourceRequest): LoadingResource { } } +async function download_resource_with_cache(request: ResourceRequest): Promise { + let response = await findCachedResponse(request); + if (!response) { + response = await fetchResource(request); + addCachedReponse(request, response); + } + + return response; +} + +const credentialsIncludeAssetBehaviors: AssetBehaviors[] = ["vfs"]; // Previously only configuration + +function fetchResource(request: ResourceRequest): Promise { + // Allow developers to override how the resource is loaded + let url = request.resolvedUrl!; + if (loaderHelpers.loadBootResource) { + const customLoadResult = invokeLoadBootResource(request); + 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; + } + } + + const fetchOptions: RequestInit = { + cache: "no-cache" + }; + + if (credentialsIncludeAssetBehaviors.includes(request.behavior)) { + // Include credentials so the server can allow download / provide user specific file + fetchOptions.credentials = "include"; + } else { + // Any other resource than configuration should provide integrity check + // 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. + fetchOptions.integrity = isCacheAvailable() ? (request.hash ?? "") : undefined; + } + + return loaderHelpers.fetch_like(url, fetchOptions); +} + +const monoToBlazorAssetTypeMap: { [key: string]: WebAssemblyBootResourceType | undefined } = { + "resource": "assembly", + "assembly": "assembly", + "pdb": "pdb", + "icu": "globalization", + "vfs": "configuration", + "dotnetwasm": "dotnetwasm", + "js-module-native": "dotnetjs", + "js-module-runtime": "dotnetjs", + "js-module-threads": "dotnetjs" +}; + +function invokeLoadBootResource(request: ResourceRequest): string | Promise | null | undefined { + if (loaderHelpers.loadBootResource) { + const requestHash = request.hash ?? ""; + const url = request.resolvedUrl!; + + // Try to send with AssetBehaviors + let customLoadResult = loaderHelpers.loadBootResource(request.behavior, request.name, url, requestHash); + if (!customLoadResult) { + // If we don't get result, try to send with WebAssemblyBootResourceType + const resourceType = monoToBlazorAssetTypeMap[request.behavior]; + if (resourceType) { + customLoadResult = loaderHelpers.loadBootResource(resourceType as AssetBehaviors, request.name, url, requestHash); + } + } + + return customLoadResult; + } + + return undefined; +} + export function cleanupAsset(asset: AssetEntryInternal) { // give GC chance to collect resources asset.pendingDownloadInternal = null as any; // GC asset.pendingDownload = null as any; // GC asset.buffer = null as any; // GC } + +function fileName(name: string) { + let lastIndexOfSlash = name.lastIndexOf("/"); + if (lastIndexOfSlash >= 0) { + lastIndexOfSlash++; + } + return name.substring(lastIndexOfSlash); +} \ No newline at end of file diff --git a/src/mono/wasm/runtime/loader/assetsCache.ts b/src/mono/wasm/runtime/loader/assetsCache.ts new file mode 100644 index 0000000..b9c44f3 --- /dev/null +++ b/src/mono/wasm/runtime/loader/assetsCache.ts @@ -0,0 +1,201 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import type { AssetBehaviors, MonoConfig, ResourceRequest } from "../types"; +import { loaderHelpers } from "./globals"; + +const cacheSkipAssetBehaviors: AssetBehaviors[] = ["vfs"]; // Previously only configuration +const usedCacheKeys: { [key: string]: boolean } = {}; +const networkLoads: { [name: string]: LoadLogEntry } = {}; +const cacheLoads: { [name: string]: LoadLogEntry } = {}; +let cacheIfUsed: Cache | null; + +export function isCacheAvailable(): boolean { + return !!cacheIfUsed; +} + +export function logDownloadStatsToConsole(): void { + const cacheLoadsEntries = Object.values(cacheLoads); + const networkLoadsEntries = Object.values(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 = loaderHelpers.config.linkerEnabled ? "%c" : "\n%cThis application was built with linking (tree shaking) disabled. Published applications will be significantly smaller if you install wasm-tools workload. See also https://aka.ms/dotnet-wasm-features"; + // eslint-disable-next-line no-console + 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) { + // eslint-disable-next-line no-console + console.groupCollapsed(`Loaded ${toDataSizeString(cacheResponseBytes)} resources from cache`); + // eslint-disable-next-line no-console + console.table(cacheLoads); + // eslint-disable-next-line no-console + console.groupEnd(); + } + + if (networkLoadsEntries.length) { + // eslint-disable-next-line no-console + console.groupCollapsed(`Loaded ${toDataSizeString(networkResponseBytes)} resources from network`); + // eslint-disable-next-line no-console + console.table(networkLoads); + // eslint-disable-next-line no-console + console.groupEnd(); + } + + // eslint-disable-next-line no-console + console.groupEnd(); +} + +export async function 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 = cacheIfUsed; + if (cache) { + const cachedRequests = await cache.keys(); + const deletionPromises = cachedRequests.map(async cachedRequest => { + if (!(cachedRequest.url in usedCacheKeys)) { + await cache.delete(cachedRequest); + } + }); + + await Promise.all(deletionPromises); + } +} + +export async function findCachedResponse(request: ResourceRequest): Promise { + const cache = cacheIfUsed; + if (!cache || cacheSkipAssetBehaviors.includes(request.behavior) || !request.hash || request.hash.length === 0) { + return undefined; + } + + const cacheKey = getCacheKey(request); + 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) { + return undefined; + } + + // It's in the cache. + const responseBytes = parseInt(cachedResponse.headers.get("content-length") || "0"); + cacheLoads[request.name] = { responseBytes }; + return cachedResponse; +} + +export function addCachedReponse(request: ResourceRequest, networkResponse: Response): void { + const cache = cacheIfUsed; + if (!cache || cacheSkipAssetBehaviors.includes(request.behavior) || !request.hash || request.hash.length === 0) { + return; + } + + const cacheKey = getCacheKey(request); + addToCacheAsync(cache, request.name, cacheKey, networkResponse); // Don't await - add to cache in background +} + +function getCacheKey(request: ResourceRequest) { + return `${request.resolvedUrl}.${request.hash}`; +} + +async function 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; + 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. + } +} + +export async function initCacheToUseIfEnabled(): Promise { + cacheIfUsed = await getCacheToUseIfEnabled(loaderHelpers.config); +} + +async function getCacheToUseIfEnabled(config: MonoConfig): Promise { + // caches will be undefined if we're running on an insecure origin (secure means https or localhost) + if (!config.cacheBootResources || typeof globalThis.caches === "undefined" || typeof globalThis.document === "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 = globalThis.document.baseURI.substring(globalThis.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/loader/blazor/BootConfig.ts b/src/mono/wasm/runtime/loader/blazor/BootConfig.ts deleted file mode 100644 index 2687b72..0000000 --- a/src/mono/wasm/runtime/loader/blazor/BootConfig.ts +++ /dev/null @@ -1,54 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -import type { BootJsonData } from "../../types/blazor"; -import type { LoadBootResourceCallback } from "../../types"; -import { loaderHelpers } from "../globals"; - -export class BootConfigResult { - private constructor(public bootConfig: BootJsonData, public applicationEnvironment: string) { - } - - static fromFetchResponse(bootConfigResponse: Response, bootConfig: BootJsonData, environment: string | undefined): BootConfigResult { - const applicationEnvironment = environment - || (loaderHelpers.getApplicationEnvironment && loaderHelpers.getApplicationEnvironment(bootConfigResponse)) - || bootConfigResponse.headers.get("Blazor-Environment") - || bootConfigResponse.headers.get("DotNet-Environment") - || "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 { - 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 { - return fetch(url, { - method: "GET", - credentials: "include", - cache: "no-cache", - }); - } - } -} - diff --git a/src/mono/wasm/runtime/loader/blazor/WebAssemblyResourceLoader.ts b/src/mono/wasm/runtime/loader/blazor/WebAssemblyResourceLoader.ts deleted file mode 100644 index cd90ffb..0000000 --- a/src/mono/wasm/runtime/loader/blazor/WebAssemblyResourceLoader.ts +++ /dev/null @@ -1,248 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -import type { LoadBootResourceCallback, WebAssemblyBootResourceType } from "../../types"; -import type { BootJsonData, ResourceList } from "../../types/blazor"; -import { loaderHelpers } from "../globals"; -import { toAbsoluteUri } from "./_Polyfill"; -const networkFetchCacheMode = "no-cache"; - -const cacheSkipResourceTypes = ["configuration"]; - -export class WebAssemblyResourceLoader { - private usedCacheKeys: { [key: string]: boolean } = {}; - - private networkLoads: { [name: string]: LoadLogEntry } = {}; - - private cacheLoads: { [name: string]: LoadLogEntry } = {}; - - static async initAsync(bootConfig: BootJsonData, loadBootResource?: LoadBootResourceCallback): Promise { - const cache = await getCacheToUseIfEnabled(bootConfig); - return new WebAssemblyResourceLoader(bootConfig, cache, loadBootResource); - } - - constructor(readonly bootConfig: BootJsonData, readonly cacheIfUsed: Cache | null, readonly loadBootResource?: LoadBootResourceCallback) { - } - - 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 && !cacheSkipResourceTypes.includes(resourceType) - ? this.loadResourceWithCaching(this.cacheIfUsed, name, url, contentHash, resourceType) - : this.loadResourceWithoutCaching(name, url, contentHash, resourceType); - - const absoluteUrl = toAbsoluteUri(url); - - if (resourceType == "assembly") { - loaderHelpers.loadedAssemblies.push(absoluteUrl); - } - return { name, url: absoluteUrl, 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."; - // eslint-disable-next-line no-console - 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) { - // eslint-disable-next-line no-console - console.groupCollapsed(`Loaded ${toDataSizeString(cacheResponseBytes)} resources from cache`); - // eslint-disable-next-line no-console - console.table(this.cacheLoads); - // eslint-disable-next-line no-console - console.groupEnd(); - } - - if (networkLoadsEntries.length) { - // eslint-disable-next-line no-console - console.groupCollapsed(`Loaded ${toDataSizeString(networkResponseBytes)} resources from network`); - // eslint-disable-next-line no-console - console.table(this.networkLoads); - // eslint-disable-next-line no-console - console.groupEnd(); - } - - // eslint-disable-next-line no-console - 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.loadBootResource) { - const customLoadResult = this.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. - const fetchOptions: RequestInit = { - cache: networkFetchCacheMode - }; - - if (resourceType === "configuration") { - // Include credentials so the server can allow download / provide user specific file - fetchOptions.credentials = "include"; - } else { - // Any other resource than configuration should provide integrity check - fetchOptions.integrity = this.bootConfig.cacheBootResources ? contentHash : undefined; - } - - return fetch(url, fetchOptions); - } - - 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/loader/blazor/_Integration.ts b/src/mono/wasm/runtime/loader/blazor/_Integration.ts deleted file mode 100644 index 2cfdaac..0000000 --- a/src/mono/wasm/runtime/loader/blazor/_Integration.ts +++ /dev/null @@ -1,314 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -import type { DotnetModuleInternal, MonoConfigInternal } from "../../types/internal"; -import { GlobalizationMode, type AssetBehaviours, type AssetEntry, type LoadBootResourceCallback, type LoadingResource, type WebAssemblyBootResourceType } from "../../types"; -import type { BootJsonData } from "../../types/blazor"; - -import { ENVIRONMENT_IS_WEB, INTERNAL, loaderHelpers } from "../globals"; -import { BootConfigResult } from "./BootConfig"; -import { WebAssemblyResourceLoader } from "./WebAssemblyResourceLoader"; -import { hasDebuggingEnabled } from "./_Polyfill"; -import { ICUDataMode } from "../../types/blazor"; -import { appendUniqueQuery } from "../assets"; - -let resourceLoader: WebAssemblyResourceLoader; - -export async function loadBootConfig(config: MonoConfigInternal, module: DotnetModuleInternal) { - const bootConfigPromise = BootConfigResult.initAsync(loaderHelpers.loadBootResource, config.applicationEnvironment); - const bootConfigResult: BootConfigResult = await bootConfigPromise; - await initializeBootConfig(bootConfigResult, module, loaderHelpers.loadBootResource); -} - -export async function initializeBootConfig(bootConfigResult: BootConfigResult, module: DotnetModuleInternal, loadBootResource?: LoadBootResourceCallback) { - INTERNAL.resourceLoader = resourceLoader = await WebAssemblyResourceLoader.initAsync(bootConfigResult.bootConfig, loadBootResource); - mapBootConfigToMonoConfig(loaderHelpers.config, bootConfigResult.applicationEnvironment); - - if (ENVIRONMENT_IS_WEB) { - setupModuleForBlazor(module); - } -} - -let resourcesLoaded = 0; -const totalResources = new Set(); - -const behaviorByName = (name: string): AssetBehaviours | "other" => { - return name === "dotnet.native.wasm" ? "dotnetwasm" - : (name.startsWith("dotnet.native.worker") && name.endsWith(".js")) ? "js-module-threads" - : (name.startsWith("dotnet.native") && name.endsWith(".js")) ? "js-module-native" - : (name.startsWith("dotnet.runtime") && name.endsWith(".js")) ? "js-module-runtime" - : (name.startsWith("dotnet") && name.endsWith(".js")) ? "js-module-dotnet" - : (name.startsWith("dotnet.native") && name.endsWith(".symbols")) ? "symbols" - : name.startsWith("icudt") ? "icu" - : "other"; -}; - -const monoToBlazorAssetTypeMap: { [key: string]: WebAssemblyBootResourceType | undefined } = { - "assembly": "assembly", - "pdb": "pdb", - "icu": "globalization", - "vfs": "configuration", - "dotnetwasm": "dotnetwasm", -}; - -export function setupModuleForBlazor(module: DotnetModuleInternal) { - // 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); - - totalResources.add(asset.name!); - res.response.then(() => { - resourcesLoaded++; - if (module.onDownloadResourceProgress) - module.onDownloadResourceProgress(resourcesLoaded, totalResources.size); - }); - - return res; - } - return undefined; - }; - - loaderHelpers.downloadResource = downloadResource; // polyfills were already assigned -} - -export function mapBootConfigToMonoConfig(moduleConfig: MonoConfigInternal, applicationEnvironment: string) { - const resources = resourceLoader.bootConfig.resources; - - const assets: AssetEntry[] = []; - const environmentVariables: any = { - // From boot config - ...(resourceLoader.bootConfig.environmentVariables || {}), - // From JavaScript - ...(moduleConfig.environmentVariables || {}) - }; - - moduleConfig.applicationEnvironment = applicationEnvironment; - - moduleConfig.remoteSources = (resourceLoader.bootConfig.resources as any).remoteSources; - moduleConfig.assetsHash = resourceLoader.bootConfig.resources.hash; - moduleConfig.assets = assets; - moduleConfig.extensions = resourceLoader.bootConfig.extensions; - moduleConfig.resources = { - extensions: resources.extensions - }; - - // Default values (when WasmDebugLevel is not set) - // - Build (debug) => debugBuild=true & debugLevel=-1 => -1 - // - Build (release) => debugBuild=true & debugLevel=0 => 0 - // - Publish (debug) => debugBuild=false & debugLevel=-1 => 0 - // - Publish (release) => debugBuild=false & debugLevel=0 => 0 - moduleConfig.debugLevel = hasDebuggingEnabled(resourceLoader.bootConfig) ? resourceLoader.bootConfig.debugLevel : 0; - moduleConfig.mainAssemblyName = resourceLoader.bootConfig.entryAssembly; - - const anyBootConfig = (resourceLoader.bootConfig as any); - for (const key in resourceLoader.bootConfig) { - if (Object.prototype.hasOwnProperty.call(anyBootConfig, key)) { - if (anyBootConfig[key] === null) { - delete anyBootConfig[key]; - } - } - } - - // FIXME this mix of both formats is ugly temporary hack - Object.assign(moduleConfig, { - ...resourceLoader.bootConfig, - }); - - moduleConfig.environmentVariables = environmentVariables; - - if (resourceLoader.bootConfig.startupMemoryCache !== undefined) { - moduleConfig.startupMemoryCache = resourceLoader.bootConfig.startupMemoryCache; - } - - if (resourceLoader.bootConfig.runtimeOptions) { - moduleConfig.runtimeOptions = [...(moduleConfig.runtimeOptions || []), ...resourceLoader.bootConfig.runtimeOptions]; - } - - // 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 = appendUniqueQuery(loaderHelpers.locateFile(name), asset.behavior); - assets.push(asset); - } - for (const name in resources.assembly) { - const asset: AssetEntry = { - name, - resolvedUrl: appendUniqueQuery(loaderHelpers.locateFile(name), "assembly"), - hash: resources.assembly[name], - behavior: "assembly", - }; - assets.push(asset); - } - if (hasDebuggingEnabled(resourceLoader.bootConfig) && resources.pdb) { - for (const name in resources.pdb) { - const asset: AssetEntry = { - name, - resolvedUrl: appendUniqueQuery(loaderHelpers.locateFile(name), "pdb"), - hash: resources.pdb[name], - behavior: "pdb", - }; - assets.push(asset); - } - } - const applicationCulture = moduleConfig.applicationCulture || (ENVIRONMENT_IS_WEB ? (navigator.languages && navigator.languages[0]) : Intl.DateTimeFormat().resolvedOptions().locale); - const icuDataResourceName = getICUResourceName(resourceLoader.bootConfig, moduleConfig, applicationCulture); - let hasIcuData = false; - for (const name in resources.runtime) { - const behavior = behaviorByName(name) as any; - let loadRemote = false; - if (behavior === "icu") { - if (resourceLoader.bootConfig.icuDataMode === ICUDataMode.Invariant) { - continue; - } - if (name !== icuDataResourceName) { - continue; - } - loadRemote = true; - hasIcuData = true; - } else if (behavior === "js-module-dotnet") { - continue; - } else if (behavior === "dotnetwasm") { - continue; - } - - const resolvedUrl = appendUniqueQuery(loaderHelpers.locateFile(name), behavior); - const asset: AssetEntry = { - name, - resolvedUrl, - hash: resources.runtime[name], - behavior, - loadRemote - }; - assets.push(asset); - } - - if (moduleConfig.loadAllSatelliteResources && resources.satelliteResources) { - for (const culture in resources.satelliteResources) { - for (const name in resources.satelliteResources[culture]) { - assets.push({ - name, - culture, - behavior: "resource", - hash: resources.satelliteResources[culture][name], - }); - } - } - } - - for (let i = 0; i < resourceLoader.bootConfig.config.length; i++) { - const config = resourceLoader.bootConfig.config[i]; - const configFileName = fileName(config); - if (configFileName === "appsettings.json" || configFileName === `appsettings.${applicationEnvironment}.json`) { - assets.push({ - name: configFileName, - resolvedUrl: appendUniqueQuery(loaderHelpers.locateFile(config), "vfs"), - behavior: "vfs", - }); - } - } - - for (const virtualPath in resources.vfs) { - for (const name in resources.vfs[virtualPath]) { - const asset: AssetEntry = { - name, - resolvedUrl: appendUniqueQuery(loaderHelpers.locateFile(name), "vfs"), - hash: resources.vfs[virtualPath][name], - behavior: "vfs", - virtualPath - }; - assets.push(asset); - } - } - - if (!hasIcuData) { - moduleConfig.globalizationMode = GlobalizationMode.Invariant; - } - - if (resourceLoader.bootConfig.modifiableAssemblies) { - // Configure the app to enable hot reload in Development. - environmentVariables["DOTNET_MODIFIABLE_ASSEMBLIES"] = resourceLoader.bootConfig.modifiableAssemblies; - } - - if (resourceLoader.bootConfig.aspnetCoreBrowserTools) { - // See https://github.com/dotnet/aspnetcore/issues/37357#issuecomment-941237000 - environmentVariables["__ASPNETCORE_BROWSER_TOOLS"] = resourceLoader.bootConfig.aspnetCoreBrowserTools; - } - - if (moduleConfig.applicationCulture) { - // If a culture is specified via start options use that to initialize the Emscripten \ .NET culture. - environmentVariables["LANG"] = `${moduleConfig.applicationCulture}.UTF-8`; - } - - if (resourceLoader.bootConfig.startupMemoryCache !== undefined) { - moduleConfig.startupMemoryCache = resourceLoader.bootConfig.startupMemoryCache; - } - - if (resourceLoader.bootConfig.runtimeOptions) { - moduleConfig.runtimeOptions = [...(moduleConfig.runtimeOptions || []), ...(resourceLoader.bootConfig.runtimeOptions || [])]; - } -} - -function fileName(name: string) { - let lastIndexOfSlash = name.lastIndexOf("/"); - if (lastIndexOfSlash >= 0) { - lastIndexOfSlash++; - } - return name.substring(lastIndexOfSlash); -} - -function getICUResourceName(bootConfig: BootJsonData, moduleConfig: MonoConfigInternal, 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) { - moduleConfig.globalizationMode = GlobalizationMode.Custom; - const customIcuFile = icuFiles[0]; - return customIcuFile; - } - } - - if (bootConfig.icuDataMode === ICUDataMode.Hybrid) { - moduleConfig.globalizationMode = GlobalizationMode.Hybrid; - const reducedICUResourceName = "icudt_hybrid.dat"; - return reducedICUResourceName; - } - - if (!culture || bootConfig.icuDataMode === ICUDataMode.All) { - moduleConfig.globalizationMode = GlobalizationMode.All; - const combinedICUResourceName = "icudt.dat"; - return combinedICUResourceName; - } - - moduleConfig.globalizationMode = GlobalizationMode.Sharded; - 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"; -} - diff --git a/src/mono/wasm/runtime/loader/blazor/_Polyfill.ts b/src/mono/wasm/runtime/loader/blazor/_Polyfill.ts deleted file mode 100644 index efe700b..0000000 --- a/src/mono/wasm/runtime/loader/blazor/_Polyfill.ts +++ /dev/null @@ -1,22 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -import type { BootJsonData } from "../../types/blazor"; -import { loaderHelpers } from "../globals"; - -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 - if (!globalThis.navigator) { - return false; - } - - const hasReferencedPdbs = !!bootConfig.resources.pdb; - return (hasReferencedPdbs || bootConfig.debugBuild || bootConfig.debugLevel != 0) && (loaderHelpers.isChromium || loaderHelpers.isFirefox); -} \ No newline at end of file diff --git a/src/mono/wasm/runtime/loader/config.ts b/src/mono/wasm/runtime/loader/config.ts index 6b1b593..f9fe128 100644 --- a/src/mono/wasm/runtime/loader/config.ts +++ b/src/mono/wasm/runtime/loader/config.ts @@ -3,24 +3,30 @@ import BuildConfiguration from "consts:configuration"; import type { DotnetModuleInternal, MonoConfigInternal } from "../types/internal"; -import type { DotnetModuleConfig } from "../types"; +import type { AssetBehaviors, DotnetModuleConfig, MonoConfig, ResourceGroups } from "../types"; import { ENVIRONMENT_IS_WEB, exportedRuntimeAPI, loaderHelpers, runtimeHelpers } from "./globals"; -import { initializeBootConfig, loadBootConfig } from "./blazor/_Integration"; -import { BootConfigResult } from "./blazor/BootConfig"; -import { BootJsonData } from "../types/blazor"; import { mono_log_error, mono_log_debug } from "./logging"; import { invokeLibraryInitializers } from "./libraryInitializers"; import { mono_exit } from "./exit"; export function deep_merge_config(target: MonoConfigInternal, source: MonoConfigInternal): MonoConfigInternal { + // If source has collection fields set to null (produced by boot config for example), we should maintain the target values const providedConfig: MonoConfigInternal = { ...source }; - if (providedConfig.assets) { + if (providedConfig.assets !== undefined) { providedConfig.assets = [...(target.assets || []), ...(providedConfig.assets || [])]; } - if (providedConfig.environmentVariables) { + if (providedConfig.resources !== undefined) { + providedConfig.resources = deep_merge_resources(target.resources || { + assembly: {}, + jsModuleNative: {}, + jsModuleRuntime: {}, + wasmNative: {} + }, providedConfig.resources); + } + if (providedConfig.environmentVariables !== undefined) { providedConfig.environmentVariables = { ...(target.environmentVariables || {}), ...(providedConfig.environmentVariables || {}) }; } - if (providedConfig.runtimeOptions) { + if (providedConfig.runtimeOptions !== undefined) { providedConfig.runtimeOptions = [...(target.runtimeOptions || []), ...(providedConfig.runtimeOptions || [])]; } return Object.assign(target, providedConfig); @@ -35,6 +41,53 @@ export function deep_merge_module(target: DotnetModuleInternal, source: DotnetMo return Object.assign(target, providedConfig); } +function deep_merge_resources(target: ResourceGroups, source: ResourceGroups): ResourceGroups { + const providedResources: ResourceGroups = { ...source }; + if (providedResources.assembly !== undefined) { + providedResources.assembly = { ...(target.assembly || {}), ...(providedResources.assembly || {}) }; + } + if (providedResources.lazyAssembly !== undefined) { + providedResources.lazyAssembly = { ...(target.lazyAssembly || {}), ...(providedResources.lazyAssembly || {}) }; + } + if (providedResources.pdb !== undefined) { + providedResources.pdb = { ...(target.pdb || {}), ...(providedResources.pdb || {}) }; + } + if (providedResources.jsModuleWorker !== undefined) { + providedResources.jsModuleWorker = { ...(target.jsModuleWorker || {}), ...(providedResources.jsModuleWorker || {}) }; + } + if (providedResources.jsModuleNative !== undefined) { + providedResources.jsModuleNative = { ...(target.jsModuleNative || {}), ...(providedResources.jsModuleNative || {}) }; + } + if (providedResources.jsModuleRuntime !== undefined) { + providedResources.jsModuleRuntime = { ...(target.jsModuleRuntime || {}), ...(providedResources.jsModuleRuntime || {}) }; + } + if (providedResources.jsSymbols !== undefined) { + providedResources.jsSymbols = { ...(target.jsSymbols || {}), ...(providedResources.jsSymbols || {}) }; + } + if (providedResources.wasmNative !== undefined) { + providedResources.wasmNative = { ...(target.wasmNative || {}), ...(providedResources.wasmNative || {}) }; + } + if (providedResources.icu !== undefined) { + providedResources.icu = { ...(target.icu || {}), ...(providedResources.icu || {}) }; + } + if (providedResources.satelliteResources !== undefined) { + providedResources.satelliteResources = { ...(target.satelliteResources || {}), ...(providedResources.satelliteResources || {}) }; + } + if (providedResources.modulesAfterConfigLoaded !== undefined) { + providedResources.modulesAfterConfigLoaded = { ...(target.modulesAfterConfigLoaded || {}), ...(providedResources.modulesAfterConfigLoaded || {}) }; + } + if (providedResources.modulesAfterRuntimeReady !== undefined) { + providedResources.modulesAfterRuntimeReady = { ...(target.modulesAfterRuntimeReady || {}), ...(providedResources.modulesAfterRuntimeReady || {}) }; + } + if (providedResources.extensions !== undefined) { + providedResources.extensions = { ...(target.extensions || {}), ...(providedResources.extensions || {}) }; + } + if (providedResources.vfs !== undefined) { + providedResources.vfs = { ...(target.vfs || {}), ...(providedResources.vfs || {}) }; + } + return Object.assign(target, providedResources); +} + // NOTE: this is called before setRuntimeGlobals export function normalizeConfig() { // normalize @@ -43,14 +96,33 @@ export function normalizeConfig() { config.environmentVariables = config.environmentVariables || {}; config.assets = config.assets || []; config.runtimeOptions = config.runtimeOptions || []; + config.resources = config.resources || { + assembly: {}, + jsModuleNative: {}, + jsModuleRuntime: {}, + wasmNative: {} + }; loaderHelpers.assertAfterExit = config.assertAfterExit = config.assertAfterExit || !ENVIRONMENT_IS_WEB; if (config.debugLevel === undefined && BuildConfiguration === "Debug") { config.debugLevel = -1; } + + // Default values (when WasmDebugLevel is not set) + // - Build (debug) => debugBuild=true & debugLevel=-1 => -1 + // - Build (release) => debugBuild=true & debugLevel=0 => 0 + // - Publish (debug) => debugBuild=false & debugLevel=-1 => 0 + // - Publish (release) => debugBuild=false & debugLevel=0 => 0 + config.debugLevel = hasDebuggingEnabled(config) ? config.debugLevel : 0; + if (config.diagnosticTracing === undefined && BuildConfiguration === "Debug") { config.diagnosticTracing = true; } + if (config.applicationCulture) { + // If a culture is specified via start options use that to initialize the Emscripten \ .NET culture. + config.environmentVariables!["LANG"] = `${config.applicationCulture}.UTF-8`; + } + runtimeHelpers.diagnosticTracing = loaderHelpers.diagnosticTracing = !!config.diagnosticTracing; runtimeHelpers.waitForDebugger = config.waitForDebugger; config.startupMemoryCache = !!config.startupMemoryCache; @@ -62,7 +134,6 @@ export function normalizeConfig() { runtimeHelpers.enablePerfMeasure = !!config.browserProfilerOptions && globalThis.performance && typeof globalThis.performance.measure === "function"; - } let configLoaded = false; @@ -80,30 +151,11 @@ export async function mono_wasm_load_config(module: DotnetModuleInternal): Promi } mono_log_debug("mono_wasm_load_config"); try { - if (loaderHelpers.loadBootResource) { - // If we have custom loadBootResource - await loadBootConfig(loaderHelpers.config, module); - } else { - // Otherwise load using fetch_like - const resolveSrc = loaderHelpers.locateFile(configFilePath); - const configResponse = await loaderHelpers.fetch_like(resolveSrc); - const loadedAnyConfig: any = (await configResponse.json()) || {}; - if (loadedAnyConfig.resources) { - // If we found boot config schema - normalizeConfig(); - await initializeBootConfig(BootConfigResult.fromFetchResponse(configResponse, loadedAnyConfig as BootJsonData, loaderHelpers.config.applicationEnvironment), module, loaderHelpers.loadBootResource); - } else { - // Otherwise we found mono config schema - const loadedConfig = loadedAnyConfig as MonoConfigInternal; - if (loadedConfig.environmentVariables && typeof (loadedConfig.environmentVariables) !== "object") - throw new Error("Expected config.environmentVariables to be unset or a dictionary-style object"); - deep_merge_config(loaderHelpers.config, loadedConfig); - } - } + await loadBootConfig(module); normalizeConfig(); - await invokeLibraryInitializers("onRuntimeConfigLoaded", [loaderHelpers.config], "onRuntimeConfigLoaded"); + await invokeLibraryInitializers("onRuntimeConfigLoaded", [loaderHelpers.config], "modulesAfterConfigLoaded"); if (module.onConfigLoaded) { try { @@ -122,4 +174,69 @@ export async function mono_wasm_load_config(module: DotnetModuleInternal): Promi mono_exit(1, new Error(errMessage)); throw err; } +} + +export function hasDebuggingEnabled(config: MonoConfigInternal): boolean { + // Copied from blazor MonoDebugger.ts/attachDebuggerHotkey + if (!globalThis.navigator) { + return false; + } + + const hasReferencedPdbs = !!config.resources!.pdb; + return (hasReferencedPdbs || config.debugLevel != 0) && (loaderHelpers.isChromium || loaderHelpers.isFirefox); +} + +async function loadBootConfig(module: DotnetModuleInternal): Promise { + const defaultConfigSrc = loaderHelpers.locateFile(module.configSrc!); + + const loaderResponse = loaderHelpers.loadBootResource !== undefined ? + loaderHelpers.loadBootResource("manifest" as AssetBehaviors, "blazor.boot.json", defaultConfigSrc, "") : + defaultLoadBootConfig(defaultConfigSrc); + + let loadConfigResponse: Response; + + if (!loaderResponse) { + loadConfigResponse = await defaultLoadBootConfig(defaultConfigSrc); + } else if (typeof loaderResponse === "string") { + loadConfigResponse = await defaultLoadBootConfig(loaderResponse); + } else { + loadConfigResponse = await loaderResponse; + } + + const loadedConfig: MonoConfig = await readBootConfigResponse(loadConfigResponse); + deep_merge_config(loaderHelpers.config, loadedConfig); + + function defaultLoadBootConfig(url: string): Promise { + return loaderHelpers.fetch_like(url, { + method: "GET", + credentials: "include", + cache: "no-cache", + }); + } +} + +async function readBootConfigResponse(loadConfigResponse: Response): Promise { + const config = loaderHelpers.config; + const loadedConfig: MonoConfig = await loadConfigResponse.json(); + + if (!config.applicationEnvironment) { + loadedConfig.applicationEnvironment = loadConfigResponse.headers.get("Blazor-Environment") || loadConfigResponse.headers.get("DotNet-Environment") || "Production"; + } + + if (!loadedConfig.environmentVariables) + loadedConfig.environmentVariables = {}; + + const modifiableAssemblies = loadConfigResponse.headers.get("DOTNET-MODIFIABLE-ASSEMBLIES"); + if (modifiableAssemblies) { + // Configure the app to enable hot reload in Development. + loadedConfig.environmentVariables["DOTNET_MODIFIABLE_ASSEMBLIES"] = modifiableAssemblies; + } + + const aspnetCoreBrowserTools = loadConfigResponse.headers.get("ASPNETCORE-BROWSER-TOOLS"); + if (aspnetCoreBrowserTools) { + // See https://github.com/dotnet/aspnetcore/issues/37357#issuecomment-941237000 + loadedConfig.environmentVariables["__ASPNETCORE_BROWSER_TOOLS"] = aspnetCoreBrowserTools; + } + + return loadedConfig; } \ No newline at end of file diff --git a/src/mono/wasm/runtime/loader/globals.ts b/src/mono/wasm/runtime/loader/globals.ts index 8905bc00..7b2225f 100644 --- a/src/mono/wasm/runtime/loader/globals.ts +++ b/src/mono/wasm/runtime/loader/globals.ts @@ -7,10 +7,11 @@ import type { AssetEntryInternal, GlobalObjects, LoaderHelpers, RuntimeHelpers } import type { MonoConfig, RuntimeAPI } from "../types"; import { assert_runtime_running, is_exited, is_runtime_running, mono_exit } from "./exit"; import { assertIsControllablePromise, createPromiseController, getPromiseController } from "./promise-controller"; -import { mono_download_assets, resolve_asset_path } from "./assets"; +import { mono_download_assets, resolve_single_asset_path, retrieve_asset_download } from "./assets"; import { setup_proxy_console } from "./logging"; -import { hasDebuggingEnabled } from "./blazor/_Polyfill"; import { invokeLibraryInitializers } from "./libraryInitializers"; +import { hasDebuggingEnabled } from "./config"; +import { logDownloadStatsToConsole, purgeUnusedCacheEntriesAsync } from "./assetsCache"; export const ENVIRONMENT_IS_NODE = typeof process == "object" && typeof process.versions == "object" && typeof process.versions.node == "string"; export const ENVIRONMENT_IS_WEB = typeof window == "object"; @@ -91,10 +92,13 @@ export function setLoaderGlobals( getPromiseController, assertIsControllablePromise, mono_download_assets, - resolve_asset_path, + resolve_asset_path: resolve_single_asset_path, setup_proxy_console, + logDownloadStatsToConsole, + purgeUnusedCacheEntriesAsync, hasDebuggingEnabled, + retrieve_asset_download, invokeLibraryInitializers, // from wasm-feature-detect npm package diff --git a/src/mono/wasm/runtime/loader/icu.ts b/src/mono/wasm/runtime/loader/icu.ts index bffc1e3..927672f 100644 --- a/src/mono/wasm/runtime/loader/icu.ts +++ b/src/mono/wasm/runtime/loader/icu.ts @@ -1,13 +1,13 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -import { GlobalizationMode } from "../types"; +import { GlobalizationMode, MonoConfig } from "../types"; import { ENVIRONMENT_IS_WEB, loaderHelpers } from "./globals"; import { mono_log_info, mono_log_debug } from "./logging"; export function init_globalization() { + loaderHelpers.preferredIcuAsset = getIcuResourceName(loaderHelpers.config); loaderHelpers.invariantMode = loaderHelpers.config.globalizationMode == GlobalizationMode.Invariant; - loaderHelpers.preferredIcuAsset = get_preferred_icu_asset(); if (!loaderHelpers.invariantMode) { if (loaderHelpers.preferredIcuAsset) { @@ -45,29 +45,43 @@ export function init_globalization() { } } -export function get_preferred_icu_asset(): string | null { - if (!loaderHelpers.config.assets || loaderHelpers.invariantMode) - return null; +export function getIcuResourceName(config: MonoConfig): string | null { + if (config.resources?.icu && config.globalizationMode != GlobalizationMode.Invariant) { + const culture = config.applicationCulture || (ENVIRONMENT_IS_WEB ? (navigator.languages && navigator.languages[0]) : Intl.DateTimeFormat().resolvedOptions().locale); - // By setting user can define what ICU source file they want to load. - // There is no need to check application's culture when is set. - // If it was not set, then we have 3 "icu" assets in config and we should choose - // only one for loading, the one that matches the application's locale. - const icuAssets = loaderHelpers.config.assets.filter(a => a["behavior"] == "icu"); - if (icuAssets.length === 1) - return icuAssets[0].name; + const icuFiles = Object.keys(config.resources.icu); - // reads the browsers locale / the OS's locale - const preferredCulture = ENVIRONMENT_IS_WEB ? navigator.language : Intl.DateTimeFormat().resolvedOptions().locale; - const prefix = preferredCulture.split("-")[0]; - const CJK = "icudt_CJK.dat"; - const EFIGS = "icudt_EFIGS.dat"; - const OTHERS = "icudt_no_CJK.dat"; + let icuFile = null; + if (config.globalizationMode === GlobalizationMode.Custom) { + if (icuFiles.length === 1) { + icuFile = icuFiles[0]; + } + } else if (config.globalizationMode === GlobalizationMode.Hybrid) { + icuFile = "icudt_hybrid.dat"; + } else if (!culture || config.globalizationMode === GlobalizationMode.All) { + icuFile = "icudt.dat"; + } else if (config.globalizationMode === GlobalizationMode.Sharded) { + icuFile = getShardedIcuResourceName(culture); + } + + if (icuFile && icuFiles.includes(icuFile)) { + return icuFile; + } + } + + config.globalizationMode = GlobalizationMode.Invariant; + return null; +} + +function getShardedIcuResourceName(culture: string): string { + 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"; + } - // not all "fr-*", "it-*", "de-*", "es-*" are in EFIGS, only the one that is mostly used - if (prefix == "en" || ["fr", "fr-FR", "it", "it-IT", "de", "de-DE", "es", "es-ES"].includes(preferredCulture)) - return EFIGS; - if (["zh", "ko", "ja"].includes(prefix)) - return CJK; - return OTHERS; + return "icudt_no_CJK.dat"; } diff --git a/src/mono/wasm/runtime/loader/libraryInitializers.ts b/src/mono/wasm/runtime/loader/libraryInitializers.ts index 87007e3..999d847 100644 --- a/src/mono/wasm/runtime/loader/libraryInitializers.ts +++ b/src/mono/wasm/runtime/loader/libraryInitializers.ts @@ -8,17 +8,17 @@ import { loaderHelpers } from "./globals"; import { mono_exit } from "./exit"; export type LibraryInitializerTypes = - "onRuntimeConfigLoaded" - | "onRuntimeReady"; + "modulesAfterConfigLoaded" + | "modulesAfterRuntimeReady"; async function fetchLibraryInitializers(config: MonoConfig, type: LibraryInitializerTypes): Promise { if (!loaderHelpers.libraryInitializers) { loaderHelpers.libraryInitializers = []; } - const libraryInitializers = type == "onRuntimeConfigLoaded" - ? config.resources?.libraryStartupModules?.onRuntimeConfigLoaded - : config.resources?.libraryStartupModules?.onRuntimeReady; + const libraryInitializers = type == "modulesAfterConfigLoaded" + ? config.resources?.modulesAfterConfigLoaded + : config.resources?.modulesAfterRuntimeReady; if (!libraryInitializers) { return; diff --git a/src/mono/wasm/runtime/loader/polyfills.ts b/src/mono/wasm/runtime/loader/polyfills.ts index bf30a52..8d2dff4 100644 --- a/src/mono/wasm/runtime/loader/polyfills.ts +++ b/src/mono/wasm/runtime/loader/polyfills.ts @@ -54,13 +54,12 @@ export async function detect_features_and_polyfill(module: DotnetModuleInternal) if (isPathAbsolute(path)) return path; return loaderHelpers.scriptDirectory + path; }; - loaderHelpers.downloadResource = module.downloadResource; loaderHelpers.fetch_like = fetch_like; // eslint-disable-next-line no-console loaderHelpers.out = console.log; // eslint-disable-next-line no-console loaderHelpers.err = console.error; - loaderHelpers.getApplicationEnvironment = module.getApplicationEnvironment; + loaderHelpers.onDownloadResourceProgress = module.onDownloadResourceProgress; if (ENVIRONMENT_IS_WEB && globalThis.navigator) { const navigator: any = globalThis.navigator; diff --git a/src/mono/wasm/runtime/loader/run.ts b/src/mono/wasm/runtime/loader/run.ts index 1709c99..82f0f99 100644 --- a/src/mono/wasm/runtime/loader/run.ts +++ b/src/mono/wasm/runtime/loader/run.ts @@ -3,19 +3,20 @@ import BuildConfiguration from "consts:configuration"; -import type { MonoConfig, DotnetHostBuilder, DotnetModuleConfig, RuntimeAPI, WebAssemblyStartOptions, LoadBootResourceCallback } from "../types"; +import type { MonoConfig, DotnetHostBuilder, DotnetModuleConfig, RuntimeAPI, LoadBootResourceCallback } from "../types"; import type { MonoConfigInternal, EmscriptenModuleInternal, RuntimeModuleExportsInternal, NativeModuleExportsInternal, } from "../types/internal"; import { ENVIRONMENT_IS_NODE, ENVIRONMENT_IS_WEB, exportedRuntimeAPI, globalObjectsRoot, mono_assert } from "./globals"; import { deep_merge_config, deep_merge_module, mono_wasm_load_config } from "./config"; import { mono_exit } from "./exit"; import { setup_proxy_console, mono_log_info } from "./logging"; -import { resolve_asset_path, start_asset_download } from "./assets"; +import { resolve_single_asset_path, start_asset_download } from "./assets"; import { detect_features_and_polyfill } from "./polyfills"; import { runtimeHelpers, loaderHelpers } from "./globals"; import { init_globalization } from "./icu"; import { setupPreloadChannelToMainThread } from "./worker"; import { invokeLibraryInitializers } from "./libraryInitializers"; +import { initCacheToUseIfEnabled } from "./assetsCache"; const module = globalObjectsRoot.module; const monoConfig = module.config as MonoConfigInternal; @@ -316,13 +317,6 @@ export class HostBuilder implements DotnetHostBuilder { } } - withStartupOptions(startupOptions: Partial): DotnetHostBuilder { - return this - .withApplicationEnvironment(startupOptions.environment) - .withApplicationCulture(startupOptions.applicationCulture) - .withResourceLoader(startupOptions.loadBootResource); - } - withApplicationEnvironment(applicationEnvironment?: string): DotnetHostBuilder { try { deep_merge_config(monoConfig, { @@ -435,8 +429,8 @@ export async function createEmscripten(moduleFactory: DotnetModuleConfig | ((api } function importModules() { - runtimeHelpers.runtimeModuleUrl = resolve_asset_path("js-module-runtime").resolvedUrl!; - runtimeHelpers.nativeModuleUrl = resolve_asset_path("js-module-native").resolvedUrl!; + runtimeHelpers.runtimeModuleUrl = resolve_single_asset_path("js-module-runtime").resolvedUrl!; + runtimeHelpers.nativeModuleUrl = resolve_single_asset_path("js-module-native").resolvedUrl!; return [ // keep js module names dynamic by using config, in the future we can use feature detection to load different flavors import(/* webpackIgnore: true */runtimeHelpers.runtimeModuleUrl), @@ -464,7 +458,7 @@ async function initializeModules(es6Modules: [RuntimeModuleExportsInternal, Nati } async function createEmscriptenMain(): Promise { - if (!module.configSrc && (!module.config || Object.keys(module.config).length === 0 || !module.config.assets)) { + if (!module.configSrc && (!module.config || Object.keys(module.config).length === 0 || !(module.config as MonoConfigInternal).assets || !module.config.resources)) { // if config file location nor assets are provided module.configSrc = "./blazor.boot.json"; } @@ -474,7 +468,9 @@ async function createEmscriptenMain(): Promise { const promises = importModules(); - const wasmModuleAsset = resolve_asset_path("dotnetwasm"); + await initCacheToUseIfEnabled(); + + const wasmModuleAsset = resolve_single_asset_path("dotnetwasm"); start_asset_download(wasmModuleAsset).then(asset => { loaderHelpers.wasmDownloadPromise.promise_control.resolve(asset); }); @@ -487,7 +483,7 @@ async function createEmscriptenMain(): Promise { await runtimeHelpers.dotnetReady.promise; - await invokeLibraryInitializers("onRuntimeReady", [globalObjectsRoot.api], "onRuntimeReady"); + await invokeLibraryInitializers("onRuntimeReady", [globalObjectsRoot.api], "modulesAfterRuntimeReady"); return exportedRuntimeAPI; } diff --git a/src/mono/wasm/runtime/satelliteAssemblies.ts b/src/mono/wasm/runtime/satelliteAssemblies.ts index 612d757..100af06 100644 --- a/src/mono/wasm/runtime/satelliteAssemblies.ts +++ b/src/mono/wasm/runtime/satelliteAssemblies.ts @@ -1,24 +1,35 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -import { INTERNAL, loaderHelpers, runtimeHelpers } from "./globals"; -import type { WebAssemblyResourceLoader } from "./loader/blazor/WebAssemblyResourceLoader"; -import { LoadingResource } from "./types"; +import { loaderHelpers, runtimeHelpers } from "./globals"; +import { AssetEntry } from "./types"; export async function loadSatelliteAssemblies(culturesToLoad: string[]): Promise { - const resourceLoader: WebAssemblyResourceLoader = INTERNAL.resourceLoader; - const satelliteResources = resourceLoader.bootConfig.resources.satelliteResources; + const satelliteResources = loaderHelpers.config.resources!.satelliteResources; if (!satelliteResources) { return; } await Promise.all(culturesToLoad! .filter(culture => Object.prototype.hasOwnProperty.call(satelliteResources, culture)) - .map(culture => resourceLoader.loadResources(satelliteResources[culture], fileName => loaderHelpers.locateFile(fileName), "assembly")) - .reduce((previous, next) => previous.concat(next), new Array()) - .map(async resource => { - const response = await resource.response; - const bytes = await response.arrayBuffer(); + .map(culture => { + const promises: Promise[] = []; + for (const name in satelliteResources[culture]) { + const asset: AssetEntry = { + name, + hash: satelliteResources[culture][name], + behavior: "resource", + culture + }; + + promises.push(loaderHelpers.retrieve_asset_download(asset)); + } + + return promises; + }) + .reduce((previous, next) => previous.concat(next), new Array>()) + .map(async bytesPromise => { + const bytes = await bytesPromise; runtimeHelpers.javaScriptExports.load_satellite_assembly(new Uint8Array(bytes)); })); } \ No newline at end of file diff --git a/src/mono/wasm/runtime/snapshot.ts b/src/mono/wasm/runtime/snapshot.ts index 6c206b9..23bf16e 100644 --- a/src/mono/wasm/runtime/snapshot.ts +++ b/src/mono/wasm/runtime/snapshot.ts @@ -142,22 +142,9 @@ async function getCacheKey(): Promise { return null; } const inputs = Object.assign({}, runtimeHelpers.config) as any; - // above already has env variables, runtime options, etc - - if (!inputs.assetsHash) { - // this is fallback for blazor which does not have assetsHash yet - inputs.assetsHash = []; - for (const asset of inputs.assets) { - if (!asset.hash) { - // if we don't have hash, we can't use the cache - return null; - } - inputs.assetsHash.push(asset.hash); - } - } - // otherwise config.assetsHash already has hashes for all the assets (DLLs, ICU, .wasms, etc). // Now we remove assets collection from the hash. + inputs.resourcesHash = inputs.resources.hash; delete inputs.assets; delete inputs.resources; // some things are calculated at runtime, so we need to add them to the hash @@ -173,7 +160,6 @@ async function getCacheKey(): Promise { delete inputs.logExitCode; delete inputs.pthreadPoolSize; delete inputs.asyncFlushOnExit; - delete inputs.assemblyRootFolder; delete inputs.remoteSources; delete inputs.ignorePdbLoadErrors; delete inputs.maxParallelDownloads; diff --git a/src/mono/wasm/runtime/startup.ts b/src/mono/wasm/runtime/startup.ts index be65c39..270f183 100644 --- a/src/mono/wasm/runtime/startup.ts +++ b/src/mono/wasm/runtime/startup.ts @@ -269,12 +269,10 @@ async function onRuntimeInitializedAsync(userOnRuntimeInitialized: () => void) { if (!runtimeHelpers.mono_wasm_runtime_is_ready) mono_wasm_runtime_ready(); - if (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 + if (loaderHelpers.config.debugLevel !== 0 && loaderHelpers.config.cacheBootResources) { + loaderHelpers.logDownloadStatsToConsole(); } + loaderHelpers.purgeUnusedCacheEntriesAsync(); // Don't await - it's fine to run in background // call user code try { diff --git a/src/mono/wasm/runtime/types/blazor.ts b/src/mono/wasm/runtime/types/blazor.ts deleted file mode 100644 index 205bac4..0000000 --- a/src/mono/wasm/runtime/types/blazor.ts +++ /dev/null @@ -1,58 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -// 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 debugLevel: number; - readonly linkerEnabled: boolean; - readonly cacheBootResources: boolean; - readonly config: string[]; - readonly icuDataMode: ICUDataMode; - readonly startupMemoryCache: boolean | undefined; - readonly runtimeOptions: string[] | undefined; - readonly environmentVariables?: { [name: string]: string }; - readonly diagnosticTracing?: boolean; - readonly pthreadPoolSize: number; - - // These properties are tacked on, and not found in the boot.json file - modifiableAssemblies: string | null; - aspnetCoreBrowserTools: string | null; - - readonly extensions?: { [name: string]: any }; -} - -export type BootJsonDataExtension = { [extensionName: string]: ResourceList }; - -export interface ResourceGroups { - readonly hash?: string; - readonly assembly: ResourceList; - readonly lazyAssembly: ResourceList; - readonly pdb?: ResourceList; - readonly runtime: ResourceList; - readonly satelliteResources?: { [cultureName: string]: ResourceList }; - readonly libraryInitializers?: ResourceList, - readonly libraryStartupModules?: { onRuntimeConfigLoaded: ResourceList, onRuntimeReady: ResourceList }, - readonly extensions?: BootJsonDataExtension - readonly runtimeAssets: ExtendedResourceList; - readonly vfs?: { [virtualPath: string]: ResourceList }; -} - -export type ResourceList = { [name: string]: string }; -export type ExtendedResourceList = { - [name: string]: { - hash: string, - behavior: string - } -}; - -export enum ICUDataMode { - Sharded = 0, - All = 1, - Invariant = 2, - Custom = 3, - Hybrid = 4 -} diff --git a/src/mono/wasm/runtime/types/index.ts b/src/mono/wasm/runtime/types/index.ts index 6222355..af76f00 100644 --- a/src/mono/wasm/runtime/types/index.ts +++ b/src/mono/wasm/runtime/types/index.ts @@ -29,14 +29,6 @@ export interface DotnetHostBuilder { // when adding new fields, please consider if it should be impacting the snapshot hash. If not, please drop it in the snapshot getCacheKey() export type MonoConfig = { /** - * The subfolder containing managed assemblies and pdbs. This is relative to dotnet.js script. - */ - assemblyRootFolder?: string, - /** - * A list of assets to load along with the runtime. - */ - assets?: AssetEntry[], - /** * Additional search locations for assets. */ remoteSources?: string[], // Sources will be checked in sequential order until the asset is found. The string "./" indicates to load from the application directory (as with the files in assembly_list), and a fully-qualified URL like "https://example.com/" indicates that asset loads can be attempted from a remote server. Sources must end with a "/". @@ -66,6 +58,11 @@ export type MonoConfig = { * debugLevel < 0 enables debugging and disables debug logging. */ debugLevel?: number, + + /** + * Gets a value that determines whether to enable caching of the 'resources' inside a CacheStorage instance within the browser. + */ + cacheBootResources?: boolean, /** * Enables diagnostic log messages during startup */ @@ -85,10 +82,6 @@ export type MonoConfig = { */ startupMemoryCache?: boolean, /** - * hash of assets - */ - assetsHash?: string, - /** * application environment */ applicationEnvironment?: string, @@ -104,6 +97,11 @@ export type MonoConfig = { resources?: ResourceGroups; /** + * appsettings files to load to VFS + */ + appsettings?: string[]; + + /** * config extensions declared in MSBuild items @(WasmBootConfigExtension) */ extensions?: { [name: string]: any }; @@ -112,22 +110,31 @@ export type MonoConfig = { export type ResourceExtensions = { [extensionName: string]: ResourceList }; export interface ResourceGroups { - readonly hash?: string; - readonly assembly?: ResourceList; // nullable only temporarily - readonly lazyAssembly?: ResourceList; // nullable only temporarily - readonly pdb?: ResourceList; - readonly runtime?: ResourceList; // nullable only temporarily - readonly satelliteResources?: { [cultureName: string]: ResourceList }; - readonly libraryInitializers?: ResourceList, - readonly libraryStartupModules?: { - readonly onRuntimeConfigLoaded?: ResourceList, - readonly onRuntimeReady?: ResourceList - }, - readonly extensions?: ResourceExtensions - readonly vfs?: { [virtualPath: string]: ResourceList }; + hash?: string; + assembly?: ResourceList; // nullable only temporarily + lazyAssembly?: ResourceList; // nullable only temporarily + pdb?: ResourceList; + + jsModuleWorker?: ResourceList; + jsModuleNative: ResourceList; + jsModuleRuntime: ResourceList; + jsSymbols?: ResourceList; + wasmNative: ResourceList; + icu?: ResourceList; + + satelliteResources?: { [cultureName: string]: ResourceList }; + + modulesAfterConfigLoaded?: ResourceList, + modulesAfterRuntimeReady?: ResourceList + + extensions?: ResourceExtensions + vfs?: { [virtualPath: string]: ResourceList }; } -export type ResourceList = { [name: string]: string }; +/** + * A "key" is name of the file, a "value" is optional hash for integrity check. + */ +export type ResourceList = { [name: string]: string | null | "" }; /** * Overrides the built-in boot resource loading mechanism so that boot resources can be fetched @@ -138,13 +145,13 @@ export type ResourceList = { [name: string]: string }; * @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. */ -export type LoadBootResourceCallback = (type: WebAssemblyBootResourceType, name: string, defaultUri: string, integrity: string) => string | Promise | null | undefined; +export type LoadBootResourceCallback = (type: AssetBehaviors | "manifest", name: string, defaultUri: string, integrity: string) => string | Promise | null | undefined; export interface ResourceRequest { name: string, // the name of the asset, including extension. - behavior: AssetBehaviours, // determines how the asset will be handled once loaded + behavior: AssetBehaviors, // determines how the asset will be handled once loaded resolvedUrl?: string; // this should be absolute url to the asset - hash?: string; + hash?: string | null | ""; // the integrity hash of the asset (if any) } export interface LoadingResource { @@ -183,7 +190,25 @@ export interface AssetEntry extends ResourceRequest { pendingDownload?: LoadingResource } -export type AssetBehaviours = +export type SingleAssetBehaviors = + /** + * The binary of the dotnet runtime. + */ + | "dotnetwasm" + /** + * The javascript module for threads. + */ + | "js-module-threads" + /** + * The javascript module for threads. + */ + | "js-module-runtime" + /** + * The javascript module for threads. + */ + | "js-module-native"; + +export type AssetBehaviors = SingleAssetBehaviors | /** * Load asset as a managed resource assembly. */ @@ -209,39 +234,19 @@ export type AssetBehaviours = */ | "vfs" /** - * The binary of the dotnet runtime. - */ - | "dotnetwasm" - /** - * The javascript module for threads. - */ - | "js-module-threads" - /** - * The javascript module for threads. - */ - | "js-module-runtime" - /** - * The javascript module for threads. - */ - | "js-module-dotnet" - /** - * The javascript module for threads. - */ - | "js-module-native" - /** * The javascript module that came from nuget package . */ | "js-module-library-initializer" /** * The javascript module for threads. */ - | "symbols" // + | "symbols" export const enum GlobalizationMode { /** * Load sharded ICU data. */ - Sharded = "sharded", // + Sharded = "sharded", /** * Load all ICU data. */ @@ -268,11 +273,9 @@ export type DotnetModuleConfig = { 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 } & Partial export type APIType = { @@ -346,29 +349,6 @@ export type ModuleAPI = { export type CreateDotnetRuntimeType = (moduleFactory: DotnetModuleConfig | ((api: RuntimeAPI) => DotnetModuleConfig)) => Promise; -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. diff --git a/src/mono/wasm/runtime/types/internal.ts b/src/mono/wasm/runtime/types/internal.ts index 66e7fb4..dda104d 100644 --- a/src/mono/wasm/runtime/types/internal.ts +++ b/src/mono/wasm/runtime/types/internal.ts @@ -1,8 +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 type { AssetBehaviours, AssetEntry, DotnetModuleConfig, LoadBootResourceCallback, LoadingResource, MonoConfig, ResourceRequest, RuntimeAPI } from "."; -import type { BootJsonData } from "./blazor"; +import type { AssetBehaviors, AssetEntry, DotnetModuleConfig, LoadBootResourceCallback, LoadingResource, MonoConfig, RuntimeAPI } from "."; import type { CharPtr, EmscriptenModule, ManagedPointer, NativePointer, VoidPtr, Int32Ptr } from "./emscripten"; export type GCHandle = { @@ -69,6 +68,8 @@ export function coerceNull(ptr: T | nu // when adding new fields, please consider if it should be impacting the snapshot hash. If not, please drop it in the snapshot getCacheKey() export type MonoConfigInternal = MonoConfig & { + linkerEnabled?: boolean, + assets?: AssetEntry[], runtimeOptions?: string[], // array of runtime options as strings aotProfilerOptions?: AOTProfilerOptions, // dictionary-style Object. If omitted, aot profiler will not be initialized. browserProfilerOptions?: BrowserProfilerOptions, // dictionary-style Object. If omitted, browser profiler will not be initialized. @@ -133,16 +134,18 @@ export type LoaderHelpers = { getPromiseController: (promise: ControllablePromise) => PromiseController, assertIsControllablePromise: (promise: Promise) => asserts promise is ControllablePromise, mono_download_assets: () => Promise, - resolve_asset_path: (behavior: AssetBehaviours) => AssetEntryInternal, + resolve_asset_path: (behavior: AssetBehaviors) => AssetEntryInternal, setup_proxy_console: (id: string, console: Console, origin: string) => void fetch_like: (url: string, init?: RequestInit) => Promise; locateFile: (path: string, prefix?: string) => string, - downloadResource?: (request: ResourceRequest) => LoadingResource | undefined out(message: string): void; err(message: string): void; - getApplicationEnvironment?: (bootConfigResponse: Response) => string | null; - hasDebuggingEnabled(bootConfig: BootJsonData): boolean, + hasDebuggingEnabled(config: MonoConfig): boolean, + retrieve_asset_download(asset: AssetEntry): Promise; + onDownloadResourceProgress?: (resourcesLoaded: number, totalResources: number) => void; + logDownloadStatsToConsole: () => void; + purgeUnusedCacheEntriesAsync: () => Promise; loadBootResource?: LoadBootResourceCallback; invokeLibraryInitializers: (functionName: string, args: any[]) => Promise, diff --git a/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/BootJsonBuilderHelper.cs b/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/BootJsonBuilderHelper.cs new file mode 100644 index 0000000..92aa853 --- /dev/null +++ b/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/BootJsonBuilderHelper.cs @@ -0,0 +1,77 @@ +// 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.IO; +using System.Linq; +using System.Text; +using Microsoft.Build.Utilities; + +namespace Microsoft.NET.Sdk.WebAssembly +{ + public class BootJsonBuilderHelper(TaskLoggingHelper Log) + { + public void ComputeResourcesHash(BootJsonData bootConfig) + { + var sb = new StringBuilder(); + + static void AddDictionary(StringBuilder sb, Dictionary? res) + { + if (res == null) + return; + + foreach (var assetHash in res.Values.OrderBy(v => v)) + sb.Append(assetHash); + } + + AddDictionary(sb, bootConfig.resources.assembly); + + AddDictionary(sb, bootConfig.resources.jsModuleWorker); + AddDictionary(sb, bootConfig.resources.jsModuleNative); + AddDictionary(sb, bootConfig.resources.jsModuleRuntime); + AddDictionary(sb, bootConfig.resources.wasmNative); + AddDictionary(sb, bootConfig.resources.jsSymbols); + AddDictionary(sb, bootConfig.resources.icu); + AddDictionary(sb, bootConfig.resources.runtime); + AddDictionary(sb, bootConfig.resources.lazyAssembly); + + if (bootConfig.resources.satelliteResources != null) + { + foreach (var culture in bootConfig.resources.satelliteResources) + AddDictionary(sb, culture.Value); + } + + if (bootConfig.resources.vfs != null) + { + foreach (var entry in bootConfig.resources.vfs) + AddDictionary(sb, entry.Value); + } + + bootConfig.resources.hash = Utils.ComputeTextIntegrity(sb.ToString()); + } + + public Dictionary? GetNativeResourceTargetInBootConfig(BootJsonData bootConfig, string resourceName) + { + string resourceExtension = Path.GetExtension(resourceName); + if (resourceName.StartsWith("dotnet.native.worker", StringComparison.OrdinalIgnoreCase) && string.Equals(resourceExtension, ".js", StringComparison.OrdinalIgnoreCase)) + return bootConfig.resources.jsModuleWorker ??= new(); + else if (resourceName.StartsWith("dotnet.native", StringComparison.OrdinalIgnoreCase) && string.Equals(resourceExtension, ".js", StringComparison.OrdinalIgnoreCase)) + return bootConfig.resources.jsModuleNative ??= new(); + else if (resourceName.StartsWith("dotnet.runtime", StringComparison.OrdinalIgnoreCase) && string.Equals(resourceExtension, ".js", StringComparison.OrdinalIgnoreCase)) + return bootConfig.resources.jsModuleRuntime ??= new(); + else if (resourceName.StartsWith("dotnet.native", StringComparison.OrdinalIgnoreCase) && string.Equals(resourceExtension, ".wasm", StringComparison.OrdinalIgnoreCase)) + return bootConfig.resources.wasmNative ??= new(); + else if (resourceName.StartsWith("dotnet", StringComparison.OrdinalIgnoreCase) && string.Equals(resourceExtension, ".js", StringComparison.OrdinalIgnoreCase)) + return null; + else if (resourceName.StartsWith("dotnet.native", StringComparison.OrdinalIgnoreCase) && string.Equals(resourceExtension, ".symbols", StringComparison.OrdinalIgnoreCase)) + return bootConfig.resources.jsSymbols ??= new(); + else if (resourceName.StartsWith("icudt", StringComparison.OrdinalIgnoreCase)) + return bootConfig.resources.icu ??= new(); + else + Log.LogError($"The resource '{resourceName}' is not recognized as any native asset"); + + return null; + } + } +} diff --git a/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/BootJsonData.cs b/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/BootJsonData.cs index 81943d6..7bb1930 100644 --- a/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/BootJsonData.cs +++ b/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/BootJsonData.cs @@ -17,8 +17,13 @@ public class BootJsonData /// /// Gets the name of the assembly with the application entry point /// + /// + /// Deprecated in .NET 8. Use + /// public string entryAssembly { get; set; } + public string mainAssemblyName { get; set; } + /// /// Gets the set of resources needed to boot the application. This includes the transitive /// closure of .NET assemblies (including the entrypoint assembly), the dotnet.wasm file, @@ -34,12 +39,12 @@ public class BootJsonData /// Gets a value that determines whether to enable caching of the /// inside a CacheStorage instance within the browser. /// - public bool cacheBootResources { get; set; } + public bool? cacheBootResources { get; set; } /// /// Gets a value that determines if this is a debug build. /// - public bool debugBuild { get; set; } + public bool? debugBuild { get; set; } /// /// Gets a value that determines what level of debugging is configured. @@ -49,17 +54,33 @@ public class BootJsonData /// /// Gets a value that determines if the linker is enabled. /// - public bool linkerEnabled { get; set; } + public bool? linkerEnabled { get; set; } /// /// Config files for the application /// + /// + /// Deprecated in .NET 8, use + /// public List config { get; set; } /// + /// Config files for the application + /// + public List appsettings { get; set; } + + /// /// Gets or sets the that determines how icu files are loaded. /// - public ICUDataMode icuDataMode { get; set; } + /// + /// Deprecated since .NET 8. Use instead. + /// + public GlobalizationMode? icuDataMode { get; set; } + + /// + /// Gets or sets the that determines how icu files are loaded. + /// + public string globalizationMode { get; set; } /// /// Gets or sets a value that determines if the caching startup memory is enabled. @@ -102,7 +123,29 @@ public class ResourcesData /// /// .NET Wasm runtime resources (dotnet.wasm, dotnet.js) etc. /// - public ResourceHashesByNameDictionary runtime { get; set; } = new ResourceHashesByNameDictionary(); + /// + /// Deprecated in .NET 8, use , , , , , . + /// + [DataMember(EmitDefaultValue = false)] + public ResourceHashesByNameDictionary runtime { get; set; } + + [DataMember(EmitDefaultValue = false)] + public ResourceHashesByNameDictionary jsModuleWorker { get; set; } + + [DataMember(EmitDefaultValue = false)] + public ResourceHashesByNameDictionary jsModuleNative { get; set; } + + [DataMember(EmitDefaultValue = false)] + public ResourceHashesByNameDictionary jsModuleRuntime { get; set; } + + [DataMember(EmitDefaultValue = false)] + public ResourceHashesByNameDictionary wasmNative { get; set; } + + [DataMember(EmitDefaultValue = false)] + public ResourceHashesByNameDictionary jsSymbols { get; set; } + + [DataMember(EmitDefaultValue = false)] + public ResourceHashesByNameDictionary icu { get; set; } /// /// "assembly" (.dll) resources @@ -134,12 +177,11 @@ public class ResourcesData [DataMember(EmitDefaultValue = false)] public ResourceHashesByNameDictionary libraryInitializers { get; set; } - /// - /// JavaScript module initializers that runtime will be in charge of loading. - /// Used in .NET >= 8 - /// [DataMember(EmitDefaultValue = false)] - public TypedLibraryStartupModules libraryStartupModules { get; set; } + public ResourceHashesByNameDictionary modulesAfterConfigLoaded { get; set; } + + [DataMember(EmitDefaultValue = false)] + public ResourceHashesByNameDictionary modulesAfterRuntimeReady { get; set; } /// /// Extensions created by users customizing the initialization process. The format of the file(s) @@ -161,19 +203,10 @@ public class ResourcesData public List remoteSources { get; set; } } -[DataContract] -public class TypedLibraryStartupModules -{ - [DataMember(EmitDefaultValue = false)] - public ResourceHashesByNameDictionary onRuntimeConfigLoaded { get; set; } - - [DataMember(EmitDefaultValue = false)] - public ResourceHashesByNameDictionary onRuntimeReady { get; set; } -} - -public enum ICUDataMode : int +public enum GlobalizationMode : int { // Note that the numeric values are serialized and used in JS code, so don't change them without also updating the JS code + // Note that names are serialized as string and used in JS code /// /// Load optimized icu data file based on the user's locale diff --git a/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/GenerateWasmBootJson.cs b/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/GenerateWasmBootJson.cs index a423251..dfa897c 100644 --- a/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/GenerateWasmBootJson.cs +++ b/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/GenerateWasmBootJson.cs @@ -10,6 +10,10 @@ using System.Reflection; using System.Runtime.Serialization; using System.Runtime.Serialization.Json; using System.Text; +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Xml.Linq; using Microsoft.Build.Framework; using Microsoft.Build.Utilities; using ResourceHashesByNameDictionary = System.Collections.Generic.Dictionary; @@ -58,9 +62,9 @@ public class GenerateWasmBootJson : Task [Required] public string TargetFrameworkVersion { get; set; } - public ITaskItem[] LibraryInitializerOnRuntimeConfigLoaded { get; set; } + public ITaskItem[] ModuleAfterConfigLoaded { get; set; } - public ITaskItem[] LibraryInitializerOnRuntimeReady { get; set; } + public ITaskItem[] ModuleAfterRuntimeReady { get; set; } [Required] public string OutputPath { get; set; } @@ -87,19 +91,36 @@ public class GenerateWasmBootJson : Task // Internal for tests public void WriteBootJson(Stream output, string entryAssemblyName) { + var helper = new BootJsonBuilderHelper(Log); + var result = new BootJsonData { - entryAssembly = entryAssemblyName, - cacheBootResources = CacheBootResources, - debugBuild = DebugBuild, - debugLevel = ParseOptionalInt(DebugLevel) ?? (DebugBuild ? 1 : 0), - linkerEnabled = LinkerEnabled, resources = new ResourcesData(), - config = new List(), - icuDataMode = GetIcuDataMode(), startupMemoryCache = ParseOptionalBool(StartupMemoryCache), }; + if (IsTargeting80OrLater()) + { + result.debugLevel = ParseOptionalInt(DebugLevel) ?? (DebugBuild ? 1 : 0); + result.mainAssemblyName = entryAssemblyName; + result.globalizationMode = GetGlobalizationMode().ToString().ToLowerInvariant(); + + if (CacheBootResources) + result.cacheBootResources = CacheBootResources; + + if (LinkerEnabled) + result.linkerEnabled = LinkerEnabled; + } + else + { + result.cacheBootResources = CacheBootResources; + result.linkerEnabled = LinkerEnabled; + result.config = new(); + result.debugBuild = DebugBuild; + result.entryAssembly = entryAssemblyName; + result.icuDataMode = GetGlobalizationMode(); + } + if (!string.IsNullOrEmpty(RuntimeOptions)) { string[] runtimeOptions = RuntimeOptions.Split(' '); @@ -127,8 +148,8 @@ public class GenerateWasmBootJson : Task result.runtimeOptions = runtimeOptions.ToArray(); } - string[] libraryInitializerOnRuntimeConfigLoadedFullPaths = LibraryInitializerOnRuntimeConfigLoaded?.Select(s => s.GetMetadata("FullPath")).ToArray() ?? Array.Empty(); - string[] libraryInitializerOnRuntimeReadyFullPath = LibraryInitializerOnRuntimeReady?.Select(s => s.GetMetadata("FullPath")).ToArray() ?? Array.Empty(); + string[] moduleAfterConfigLoadedFullPaths = ModuleAfterConfigLoaded?.Select(s => s.GetMetadata("FullPath")).ToArray() ?? Array.Empty(); + string[] moduleAfterRuntimeReadyFullPaths = ModuleAfterRuntimeReady?.Select(s => s.GetMetadata("FullPath")).ToArray() ?? Array.Empty(); // Build a two-level dictionary of the form: // - assembly: @@ -163,7 +184,9 @@ public class GenerateWasmBootJson : Task { Log.LogMessage(MessageImportance.Low, "Candidate '{0}' is defined as satellite assembly with culture '{1}'.", resource.ItemSpec, assetTraitValue); resourceData.satelliteResources ??= new Dictionary(StringComparer.OrdinalIgnoreCase); - resourceName = assetTraitValue + "/" + resourceName; + + if (!IsTargeting80OrLater()) + resourceName = assetTraitValue + "/" + resourceName; if (!resourceData.satelliteResources.TryGetValue(assetTraitValue, out resourceList)) { @@ -195,12 +218,20 @@ public class GenerateWasmBootJson : Task string.Equals(assetTraitValue, "native", StringComparison.OrdinalIgnoreCase)) { Log.LogMessage(MessageImportance.Low, "Candidate '{0}' is defined as a native application resource.", resource.ItemSpec); - if (fileName.StartsWith("dotnet", StringComparison.OrdinalIgnoreCase) && string.Equals(fileExtension, ".wasm", StringComparison.OrdinalIgnoreCase)) + + if (IsTargeting80OrLater()) { - behavior = "dotnetwasm"; + resourceList = helper.GetNativeResourceTargetInBootConfig(result, resourceName); } + else + { + if (fileName.StartsWith("dotnet", StringComparison.OrdinalIgnoreCase) && string.Equals(fileExtension, ".wasm", StringComparison.OrdinalIgnoreCase)) + { + behavior = "dotnetwasm"; + } - resourceList = resourceData.runtime; + resourceList = resourceData.runtime ??= new(); + } } else if (string.Equals("JSModule", assetTraitName, StringComparison.OrdinalIgnoreCase) && string.Equals(assetTraitValue, "JSLibraryModule", StringComparison.OrdinalIgnoreCase)) @@ -215,27 +246,25 @@ public class GenerateWasmBootJson : Task if (IsTargeting80OrLater()) { - var libraryStartupModules = resourceData.libraryStartupModules ??= new TypedLibraryStartupModules(); - - if (libraryInitializerOnRuntimeConfigLoadedFullPaths.Contains(resource.ItemSpec)) + if (moduleAfterConfigLoadedFullPaths.Contains(resource.ItemSpec)) { - resourceList = libraryStartupModules.onRuntimeConfigLoaded ??= new(); + resourceList = resourceData.modulesAfterConfigLoaded ??= new(); } - else if (libraryInitializerOnRuntimeReadyFullPath.Contains(resource.ItemSpec)) + else if (moduleAfterRuntimeReadyFullPaths.Contains(resource.ItemSpec)) { - resourceList = libraryStartupModules.onRuntimeReady ??= new(); + resourceList = resourceData.modulesAfterRuntimeReady ??= new(); } else if (File.Exists(resource.ItemSpec)) { string fileContent = File.ReadAllText(resource.ItemSpec); if (fileContent.Contains("onRuntimeConfigLoaded") || fileContent.Contains("beforeStart") || fileContent.Contains("afterStarted")) - resourceList = libraryStartupModules.onRuntimeConfigLoaded ??= new(); + resourceList = resourceData.modulesAfterConfigLoaded ??= new(); else - resourceList = libraryStartupModules.onRuntimeReady ??= new(); + resourceList = resourceData.modulesAfterRuntimeReady ??= new(); } else { - resourceList = libraryStartupModules.onRuntimeConfigLoaded ??= new(); + resourceList = resourceData.modulesAfterConfigLoaded ??= new(); } string newTargetPath = "../" + targetPath; // This needs condition once WasmRuntimeAssetsLocation is supported in Wasm SDK @@ -306,38 +335,40 @@ public class GenerateWasmBootJson : Task { string configUrl = Path.GetFileName(configFile.ItemSpec); if (IsTargeting80OrLater()) - configUrl = "../" + configUrl; // This needs condition once WasmRuntimeAssetsLocation is supported in Wasm SDK + { + result.appsettings ??= new(); - result.config.Add(configUrl); + configUrl = "../" + configUrl; // This needs condition once WasmRuntimeAssetsLocation is supported in Wasm SDK + result.appsettings.Add(configUrl); + } + else + { + result.config.Add(configUrl); + } } } - if (Extensions != null && Extensions.Length > 0) + var jsonOptions = new JsonSerializerOptions() { - var configSerializer = new DataContractJsonSerializer(typeof(Dictionary), new DataContractJsonSerializerSettings - { - UseSimpleDictionaryFormat = true - }); + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + WriteIndented = true + }; + if (Extensions != null && Extensions.Length > 0) + { result.extensions = new Dictionary>(); foreach (var configExtension in Extensions) { var key = configExtension.GetMetadata("key"); using var fs = File.OpenRead(configExtension.ItemSpec); - var config = (Dictionary)configSerializer.ReadObject(fs); + var config = JsonSerializer.Deserialize>(fs, jsonOptions); result.extensions[key] = config; } } - var serializer = new DataContractJsonSerializer(typeof(BootJsonData), new DataContractJsonSerializerSettings - { - UseSimpleDictionaryFormat = true, - KnownTypes = new[] { typeof(TypedLibraryStartupModules) }, - EmitTypeInformation = EmitTypeInformation.Never - }); - - using var writer = JsonReaderWriterFactory.CreateJsonWriter(output, Encoding.UTF8, ownsStream: false, indent: true); - serializer.WriteObject(writer, result); + helper.ComputeResourcesHash(result); + JsonSerializer.Serialize(output, result, jsonOptions); void AddResourceToList(ITaskItem resource, ResourceHashesByNameDictionary resourceList, string resourceKey) { @@ -349,18 +380,18 @@ public class GenerateWasmBootJson : Task } } - private ICUDataMode GetIcuDataMode() + private GlobalizationMode GetGlobalizationMode() { if (string.Equals(InvariantGlobalization, "true", StringComparison.OrdinalIgnoreCase)) - return ICUDataMode.Invariant; + return GlobalizationMode.Invariant; else if (IsHybridGlobalization) - return ICUDataMode.Hybrid; + return GlobalizationMode.Hybrid; else if (LoadAllICUData) - return ICUDataMode.All; + return GlobalizationMode.All; else if (LoadCustomIcuData) - return ICUDataMode.Custom; + return GlobalizationMode.Custom; - return ICUDataMode.Sharded; + return GlobalizationMode.Sharded; } private static bool? ParseOptionalBool(string value) diff --git a/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks.csproj b/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks.csproj index 747e216..2f6ef98 100644 --- a/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks.csproj +++ b/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks.csproj @@ -20,6 +20,7 @@ + diff --git a/src/tasks/WasmAppBuilder/WasmAppBuilder.cs b/src/tasks/WasmAppBuilder/WasmAppBuilder.cs index 12e7cb3..9c7d8a6 100644 --- a/src/tasks/WasmAppBuilder/WasmAppBuilder.cs +++ b/src/tasks/WasmAppBuilder/WasmAppBuilder.cs @@ -8,6 +8,7 @@ using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Text; +using System.Text.Encodings.Web; using System.Text.Json; using System.Text.Json.Nodes; using System.Text.Json.Serialization; @@ -58,30 +59,32 @@ public class WasmAppBuilder : WasmAppBuilderBaseTask return true; } - private ICUDataMode GetICUDataMode() + private GlobalizationMode GetGlobalizationMode() { // Invariant has always precedence if (InvariantGlobalization) - return ICUDataMode.Invariant; + return GlobalizationMode.Invariant; // If user provided a path to a custom ICU data file, use it if (!string.IsNullOrEmpty(WasmIcuDataFileName)) - return ICUDataMode.Custom; + return GlobalizationMode.Custom; // Hybrid mode if (HybridGlobalization) - return ICUDataMode.Hybrid; + return GlobalizationMode.Hybrid; // If user requested to include full ICU data, use it if (WasmIncludeFullIcuData) - return ICUDataMode.All; + return GlobalizationMode.All; // Otherwise, use sharded mode - return ICUDataMode.Sharded; + return GlobalizationMode.Sharded; } protected override bool ExecuteInternal() { + var helper = new BootJsonBuilderHelper(Log); + if (!ValidateArguments()) return false; @@ -95,9 +98,8 @@ public class WasmAppBuilder : WasmAppBuilderBaseTask var bootConfig = new BootJsonData() { - config = new(), - entryAssembly = MainAssemblyName, - icuDataMode = GetICUDataMode() + mainAssemblyName = MainAssemblyName, + globalizationMode = GetGlobalizationMode().ToString().ToLowerInvariant() }; // Create app @@ -158,19 +160,9 @@ public class WasmAppBuilder : WasmAppBuilderBaseTask var itemHash = Utils.ComputeIntegrity(item.ItemSpec); - if (name.StartsWith("dotnet", StringComparison.OrdinalIgnoreCase) && string.Equals(Path.GetExtension(name), ".wasm", StringComparison.OrdinalIgnoreCase)) - { - if (bootConfig.resources.runtimeAssets == null) - bootConfig.resources.runtimeAssets = new(); - - bootConfig.resources.runtimeAssets[name] = new() - { - hash = itemHash, - behavior = "dotnetwasm" - }; - } - - bootConfig.resources.runtime[name] = itemHash; + Dictionary? resourceList = helper.GetNativeResourceTargetInBootConfig(bootConfig, name); + if (resourceList != null) + resourceList[name] = itemHash; } string packageJsonPath = Path.Combine(AppDir, "package.json"); @@ -213,7 +205,6 @@ public class WasmAppBuilder : WasmAppBuilderBaseTask } } - bootConfig.debugBuild = DebugLevel > 0; bootConfig.debugLevel = DebugLevel; ProcessSatelliteAssemblies(args => @@ -312,7 +303,8 @@ public class WasmAppBuilder : WasmAppBuilderBaseTask return false; } - bootConfig.resources.runtime[Path.GetFileName(idfn)] = Utils.ComputeIntegrity(idfn); + bootConfig.resources.icu ??= new(); + bootConfig.resources.icu[Path.GetFileName(idfn)] = Utils.ComputeIntegrity(idfn); } } @@ -370,35 +362,15 @@ public class WasmAppBuilder : WasmAppBuilderBaseTask string tmpMonoConfigPath = Path.GetTempFileName(); using (var sw = File.CreateText(tmpMonoConfigPath)) { - var sb = new StringBuilder(); + helper.ComputeResourcesHash(bootConfig); - static void AddDictionary(StringBuilder sb, Dictionary res) + var jsonOptions = new JsonSerializerOptions { - foreach (var asset in res) - sb.Append(asset.Value); - } - - AddDictionary(sb, bootConfig.resources.assembly); - AddDictionary(sb, bootConfig.resources.runtime); - - if (bootConfig.resources.lazyAssembly != null) - AddDictionary(sb, bootConfig.resources.lazyAssembly); - - if (bootConfig.resources.satelliteResources != null) - { - foreach (var culture in bootConfig.resources.satelliteResources) - AddDictionary(sb, culture.Value); - } - - if (bootConfig.resources.vfs != null) - { - foreach (var entry in bootConfig.resources.vfs) - AddDictionary(sb, entry.Value); - } - - bootConfig.resources.hash = Utils.ComputeTextIntegrity(sb.ToString()); - - var json = JsonSerializer.Serialize(bootConfig, new JsonSerializerOptions { WriteIndented = true }); + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + WriteIndented = true + }; + var json = JsonSerializer.Serialize(bootConfig, jsonOptions); sw.Write(json); } diff --git a/src/tasks/WasmAppBuilder/WasmAppBuilder.csproj b/src/tasks/WasmAppBuilder/WasmAppBuilder.csproj index a873b4d..67473b9 100644 --- a/src/tasks/WasmAppBuilder/WasmAppBuilder.csproj +++ b/src/tasks/WasmAppBuilder/WasmAppBuilder.csproj @@ -20,6 +20,7 @@ + -- 2.7.4