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 593825f84be8e3..be765da719e5b2 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)" - ModuleAfterConfigLoaded="@(WasmModuleAfterConfigLoaded)" - ModuleAfterRuntimeReady="@(WasmModuleAfterRuntimeReady)" /> + LibraryInitializerOnRuntimeConfigLoaded="@(WasmLibraryInitializerOnRuntimeConfigLoaded)" + LibraryInitializerOnRuntimeReady="@(WasmLibraryInitializerOnRuntimeReady)" /> @@ -563,8 +563,8 @@ Copyright (c) .NET Foundation. All rights reserved. RuntimeOptions="$(_BlazorWebAssemblyRuntimeOptions)" Extensions="@(WasmBootConfigExtension)" TargetFrameworkVersion="$(TargetFrameworkVersion)" - ModuleAfterConfigLoaded="@(WasmModuleAfterConfigLoaded)" - ModuleAfterRuntimeReady="@(WasmModuleAfterRuntimeReady)" /> + LibraryInitializerOnRuntimeConfigLoaded="@(WasmLibraryInitializerOnRuntimeConfigLoaded)" + LibraryInitializerOnRuntimeReady="@(WasmLibraryInitializerOnRuntimeReady)" /> diff --git a/src/mono/wasm/Wasm.Build.Tests/ProjectProviderBase.cs b/src/mono/wasm/Wasm.Build.Tests/ProjectProviderBase.cs index 40d6a2a2ecd8f3..1b571327dfd0ff 100644 --- a/src/mono/wasm/Wasm.Build.Tests/ProjectProviderBase.cs +++ b/src/mono/wasm/Wasm.Build.Tests/ProjectProviderBase.cs @@ -390,12 +390,7 @@ public void AssertBootJson(AssertBundleOptionsBase options) Assert.True(File.Exists(bootJsonPath), $"Expected to find {bootJsonPath}"); BootJsonData bootJson = ParseBootData(bootJsonPath); - 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 bootJsonEntries = bootJson.resources.runtime.Keys.Where(k => k.StartsWith("dotnet.", StringComparison.Ordinal)).ToArray(); var expectedEntries = new SortedDictionary>(); IReadOnlySet expected = GetDotNetFilesExpectedSet(options); @@ -403,8 +398,7 @@ public void AssertBootJson(AssertBundleOptionsBase options) var knownSet = GetAllKnownDotnetFilesToFingerprintMap(options); foreach (string expectedFilename in expected) { - // FIXME: Find a systematic solution for skipping dotnet.js from boot json check - if (expectedFilename == "dotnet.js" || Path.GetExtension(expectedFilename) == ".map") + if (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 3bb25afcdb06c4..e42d6e190f4c8c 100644 --- a/src/mono/wasm/runtime/dotnet.d.ts +++ b/src/mono/wasm/runtime/dotnet.d.ts @@ -95,6 +95,14 @@ interface DotnetHostBuilder { run(): Promise; } 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. */ @@ -125,10 +133,6 @@ 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 */ @@ -147,6 +151,10 @@ type MonoConfig = { * If true, the snapshot of runtime's memory will be stored in the browser and used for faster startup next time. Default is false. */ startupMemoryCache?: boolean; + /** + * hash of assets + */ + assetsHash?: string; /** * application environment */ @@ -159,10 +167,6 @@ type MonoConfig = { * definition of assets to load along with the runtime. */ resources?: ResourceGroups; - /** - * appsettings files to load to VFS - */ - appsettings?: string[]; /** * config extensions declared in MSBuild items @(WasmBootConfigExtension) */ @@ -174,31 +178,26 @@ type ResourceExtensions = { [extensionName: string]: ResourceList; }; interface ResourceGroups { - hash?: string; - assembly?: ResourceList; - lazyAssembly?: ResourceList; - pdb?: ResourceList; - jsModuleWorker?: ResourceList; - jsModuleNative: ResourceList; - jsModuleRuntime: ResourceList; - jsSymbols?: ResourceList; - wasmNative: ResourceList; - icu?: ResourceList; - satelliteResources?: { + readonly hash?: string; + readonly assembly?: ResourceList; + readonly lazyAssembly?: ResourceList; + readonly pdb?: ResourceList; + readonly runtime?: ResourceList; + readonly satelliteResources?: { [cultureName: string]: ResourceList; }; - modulesAfterConfigLoaded?: ResourceList; - modulesAfterRuntimeReady?: ResourceList; - extensions?: ResourceExtensions; - vfs?: { + readonly libraryInitializers?: ResourceList; + readonly libraryStartupModules?: { + readonly onRuntimeConfigLoaded?: ResourceList; + readonly onRuntimeReady?: ResourceList; + }; + readonly extensions?: ResourceExtensions; + readonly vfs?: { [virtualPath: string]: ResourceList; }; } -/** - * A "key" is name of the file, a "value" is optional hash for integrity check. - */ type ResourceList = { - [name: string]: string | null | ""; + [name: string]: string; }; /** * Overrides the built-in boot resource loading mechanism so that boot resources can be fetched @@ -209,12 +208,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: AssetBehaviors | "manifest", name: string, defaultUri: string, integrity: string) => string | Promise | null | undefined; +type LoadBootResourceCallback = (type: WebAssemblyBootResourceType, name: string, defaultUri: string, integrity: string) => string | Promise | null | undefined; interface ResourceRequest { name: string; - behavior: AssetBehaviors; + behavior: AssetBehaviours; resolvedUrl?: string; - hash?: string | null | ""; + hash?: string; } interface LoadingResource { name: string; @@ -249,24 +248,7 @@ interface AssetEntry extends ResourceRequest { */ pendingDownload?: LoadingResource; } -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 | +type AssetBehaviours = /** * Load asset as a managed resource assembly. */ @@ -291,6 +273,26 @@ type AssetBehaviors = SingleAssetBehaviors | * Load asset into the virtual filesystem (for fopen, File.Open, etc). */ | "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 . */ @@ -328,8 +330,10 @@ 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; @@ -396,6 +400,7 @@ 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 59c153a9d62365..8402e7f37e2217 100644 --- a/src/mono/wasm/runtime/lazyLoading.ts +++ b/src/mono/wasm/runtime/lazyLoading.ts @@ -1,50 +1,40 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -import { loaderHelpers, runtimeHelpers } from "./globals"; -import { AssetEntry } from "./types"; +import { INTERNAL, loaderHelpers, runtimeHelpers } from "./globals"; +import type { WebAssemblyResourceLoader } from "./loader/blazor/WebAssemblyResourceLoader"; export async function loadLazyAssembly(assemblyNameToLoad: string): Promise { - const resources = loaderHelpers.config.resources!; + const resourceLoader: WebAssemblyResourceLoader = INTERNAL.resourceLoader; + const resources = resourceLoader.bootConfig.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."); } - if (!lazyAssemblies[assemblyNameToLoad]) { + const assemblyMarkedAsLazy = Object.prototype.hasOwnProperty.call(lazyAssemblies, assemblyNameToLoad); + if (!assemblyMarkedAsLazy) { 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 pdbNameToLoad = changeExtension(dllAsset.name, ".pdb"); - const shouldLoadPdb = loaderHelpers.hasDebuggingEnabled(loaderHelpers.config) && Object.prototype.hasOwnProperty.call(lazyAssemblies, pdbNameToLoad); + const dllNameToLoad = assemblyNameToLoad; + const pdbNameToLoad = changeExtension(assemblyNameToLoad, ".pdb"); + const shouldLoadPdb = loaderHelpers.hasDebuggingEnabled(resourceLoader.bootConfig) && resources.pdb && Object.prototype.hasOwnProperty.call(lazyAssemblies, pdbNameToLoad); - const dllBytesPromise = loaderHelpers.retrieve_asset_download(dllAsset); + const dllBytesPromise = resourceLoader.loadResource(dllNameToLoad, loaderHelpers.locateFile(dllNameToLoad), lazyAssemblies[dllNameToLoad], "assembly").response.then(response => response.arrayBuffer()); let dll = null; let pdb = null; if (shouldLoadPdb) { - const pdbBytesPromise = lazyAssemblies[pdbNameToLoad] - ? loaderHelpers.retrieve_asset_download({ - name: pdbNameToLoad, - hash: lazyAssemblies[pdbNameToLoad], - behavior: "pdb" - }) - : Promise.resolve(null); - + const pdbBytesPromise = await resourceLoader.loadResource(pdbNameToLoad, loaderHelpers.locateFile(pdbNameToLoad), lazyAssemblies[pdbNameToLoad], "pdb").response.then(response => response.arrayBuffer()); const [dllBytes, pdbBytes] = await Promise.all([dllBytesPromise, pdbBytesPromise]); dll = new Uint8Array(dllBytes); - pdb = pdbBytes ? new Uint8Array(pdbBytes) : null; + pdb = new Uint8Array(pdbBytes); } 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 92c944e9752939..5cf6ec96ea8a07 100644 --- a/src/mono/wasm/runtime/loader/assets.ts +++ b/src/mono/wasm/runtime/loader/assets.ts @@ -2,13 +2,11 @@ // The .NET Foundation licenses this file to you under the MIT license. import type { AssetEntryInternal, PromiseAndController } from "../types/internal"; -import type { AssetBehaviors, AssetEntry, LoadingResource, ResourceList, ResourceRequest, SingleAssetBehaviors as SingleAssetBehaviors, WebAssemblyBootResourceType } from "../types"; +import type { AssetBehaviours, AssetEntry, LoadingResource, ResourceRequest } 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; @@ -21,6 +19,7 @@ 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 @@ -64,47 +63,14 @@ export function shouldLoadIcuAsset(asset: AssetEntryInternal): boolean { return !(asset.behavior == "icu" && asset.name != loaderHelpers.preferredIcuAsset); } -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.`); +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, ""); } - 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; @@ -114,7 +80,20 @@ export async function mono_download_assets(): Promise { const containedInSnapshotAssets: AssetEntryInternal[] = []; const promises_of_assets: Promise[] = []; - prepareAssets(containedInSnapshotAssets, alwaysLoadedAssets); + 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); + } + } const countAndStartDownload = (asset: AssetEntryInternal) => { if (!skipInstantiateByAssetTypes[asset.behavior] && shouldLoadIcuAsset(asset)) { @@ -218,132 +197,10 @@ 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 { @@ -496,14 +353,19 @@ 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 = asset.name; + attemptUrl = assemblyRootFolder + ? (assemblyRootFolder + "/" + asset.name) + : asset.name; } else if (asset.behavior === "resource") { const path = asset.culture && asset.culture !== "" ? `${asset.culture}/${asset.name}` : asset.name; - attemptUrl = path; + attemptUrl = assemblyRootFolder + ? (assemblyRootFolder + "/" + path) + : path; } else { attemptUrl = asset.name; @@ -520,7 +382,7 @@ function resolve_path(asset: AssetEntry, sourcePrefix: string): string { return attemptUrl; } -export function appendUniqueQuery(attemptUrl: string, behavior: AssetBehaviors): string { +export function appendUniqueQuery(attemptUrl: string, behavior: AssetBehaviours): 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; @@ -529,26 +391,22 @@ export function appendUniqueQuery(attemptUrl: string, behavior: AssetBehaviors): return attemptUrl; } -let resourcesLoaded = 0; -const totalResources = new Set(); + function download_resource(request: ResourceRequest): LoadingResource { try { - 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; + 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 + }; } catch (err) { const response = { ok: false, @@ -564,94 +422,9 @@ 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 deleted file mode 100644 index b9c44f3fa3ef5d..00000000000000 --- a/src/mono/wasm/runtime/loader/assetsCache.ts +++ /dev/null @@ -1,201 +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 { 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 new file mode 100644 index 00000000000000..2687b72b029338 --- /dev/null +++ b/src/mono/wasm/runtime/loader/blazor/BootConfig.ts @@ -0,0 +1,54 @@ +// 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 new file mode 100644 index 00000000000000..cd90ffb2b9a7ed --- /dev/null +++ b/src/mono/wasm/runtime/loader/blazor/WebAssemblyResourceLoader.ts @@ -0,0 +1,248 @@ +// 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 new file mode 100644 index 00000000000000..2cfdaac4e4f75c --- /dev/null +++ b/src/mono/wasm/runtime/loader/blazor/_Integration.ts @@ -0,0 +1,314 @@ +// 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 new file mode 100644 index 00000000000000..efe700b72bcca3 --- /dev/null +++ b/src/mono/wasm/runtime/loader/blazor/_Polyfill.ts @@ -0,0 +1,22 @@ +// 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 f9fe128537f2ac..6b1b5932a39059 100644 --- a/src/mono/wasm/runtime/loader/config.ts +++ b/src/mono/wasm/runtime/loader/config.ts @@ -3,30 +3,24 @@ import BuildConfiguration from "consts:configuration"; import type { DotnetModuleInternal, MonoConfigInternal } from "../types/internal"; -import type { AssetBehaviors, DotnetModuleConfig, MonoConfig, ResourceGroups } from "../types"; +import type { DotnetModuleConfig } 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 !== undefined) { + if (providedConfig.assets) { providedConfig.assets = [...(target.assets || []), ...(providedConfig.assets || [])]; } - if (providedConfig.resources !== undefined) { - providedConfig.resources = deep_merge_resources(target.resources || { - assembly: {}, - jsModuleNative: {}, - jsModuleRuntime: {}, - wasmNative: {} - }, providedConfig.resources); - } - if (providedConfig.environmentVariables !== undefined) { + if (providedConfig.environmentVariables) { providedConfig.environmentVariables = { ...(target.environmentVariables || {}), ...(providedConfig.environmentVariables || {}) }; } - if (providedConfig.runtimeOptions !== undefined) { + if (providedConfig.runtimeOptions) { providedConfig.runtimeOptions = [...(target.runtimeOptions || []), ...(providedConfig.runtimeOptions || [])]; } return Object.assign(target, providedConfig); @@ -41,53 +35,6 @@ 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 @@ -96,33 +43,14 @@ 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; @@ -134,6 +62,7 @@ export function normalizeConfig() { runtimeHelpers.enablePerfMeasure = !!config.browserProfilerOptions && globalThis.performance && typeof globalThis.performance.measure === "function"; + } let configLoaded = false; @@ -151,11 +80,30 @@ export async function mono_wasm_load_config(module: DotnetModuleInternal): Promi } mono_log_debug("mono_wasm_load_config"); try { - await loadBootConfig(module); + 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); + } + } normalizeConfig(); - await invokeLibraryInitializers("onRuntimeConfigLoaded", [loaderHelpers.config], "modulesAfterConfigLoaded"); + await invokeLibraryInitializers("onRuntimeConfigLoaded", [loaderHelpers.config], "onRuntimeConfigLoaded"); if (module.onConfigLoaded) { try { @@ -174,69 +122,4 @@ 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 7b2225f1c84ea6..8905bc00139f58 100644 --- a/src/mono/wasm/runtime/loader/globals.ts +++ b/src/mono/wasm/runtime/loader/globals.ts @@ -7,11 +7,10 @@ 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_single_asset_path, retrieve_asset_download } from "./assets"; +import { mono_download_assets, resolve_asset_path } 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"; @@ -92,13 +91,10 @@ export function setLoaderGlobals( getPromiseController, assertIsControllablePromise, mono_download_assets, - resolve_asset_path: resolve_single_asset_path, + resolve_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 927672f5db99d6..bffc1e3dd13460 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, MonoConfig } from "../types"; +import { GlobalizationMode } 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,43 +45,29 @@ export function init_globalization() { } } -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); +export function get_preferred_icu_asset(): string | null { + if (!loaderHelpers.config.assets || loaderHelpers.invariantMode) + return null; - const icuFiles = Object.keys(config.resources.icu); + // 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; - 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"; - } + // 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"; - return "icudt_no_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; } diff --git a/src/mono/wasm/runtime/loader/libraryInitializers.ts b/src/mono/wasm/runtime/loader/libraryInitializers.ts index 999d847e2755a2..87007e35e98cd7 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 = - "modulesAfterConfigLoaded" - | "modulesAfterRuntimeReady"; + "onRuntimeConfigLoaded" + | "onRuntimeReady"; async function fetchLibraryInitializers(config: MonoConfig, type: LibraryInitializerTypes): Promise { if (!loaderHelpers.libraryInitializers) { loaderHelpers.libraryInitializers = []; } - const libraryInitializers = type == "modulesAfterConfigLoaded" - ? config.resources?.modulesAfterConfigLoaded - : config.resources?.modulesAfterRuntimeReady; + const libraryInitializers = type == "onRuntimeConfigLoaded" + ? config.resources?.libraryStartupModules?.onRuntimeConfigLoaded + : config.resources?.libraryStartupModules?.onRuntimeReady; if (!libraryInitializers) { return; diff --git a/src/mono/wasm/runtime/loader/polyfills.ts b/src/mono/wasm/runtime/loader/polyfills.ts index 8d2dff49825e76..bf30a526601cd2 100644 --- a/src/mono/wasm/runtime/loader/polyfills.ts +++ b/src/mono/wasm/runtime/loader/polyfills.ts @@ -54,12 +54,13 @@ 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.onDownloadResourceProgress = module.onDownloadResourceProgress; + loaderHelpers.getApplicationEnvironment = module.getApplicationEnvironment; 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 82f0f9988b8d12..1709c99571535d 100644 --- a/src/mono/wasm/runtime/loader/run.ts +++ b/src/mono/wasm/runtime/loader/run.ts @@ -3,20 +3,19 @@ import BuildConfiguration from "consts:configuration"; -import type { MonoConfig, DotnetHostBuilder, DotnetModuleConfig, RuntimeAPI, LoadBootResourceCallback } from "../types"; +import type { MonoConfig, DotnetHostBuilder, DotnetModuleConfig, RuntimeAPI, WebAssemblyStartOptions, 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_single_asset_path, start_asset_download } from "./assets"; +import { resolve_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; @@ -317,6 +316,13 @@ 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, { @@ -429,8 +435,8 @@ export async function createEmscripten(moduleFactory: DotnetModuleConfig | ((api } function importModules() { - runtimeHelpers.runtimeModuleUrl = resolve_single_asset_path("js-module-runtime").resolvedUrl!; - runtimeHelpers.nativeModuleUrl = resolve_single_asset_path("js-module-native").resolvedUrl!; + runtimeHelpers.runtimeModuleUrl = resolve_asset_path("js-module-runtime").resolvedUrl!; + runtimeHelpers.nativeModuleUrl = resolve_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), @@ -458,7 +464,7 @@ async function initializeModules(es6Modules: [RuntimeModuleExportsInternal, Nati } async function createEmscriptenMain(): Promise { - if (!module.configSrc && (!module.config || Object.keys(module.config).length === 0 || !(module.config as MonoConfigInternal).assets || !module.config.resources)) { + if (!module.configSrc && (!module.config || Object.keys(module.config).length === 0 || !module.config.assets)) { // if config file location nor assets are provided module.configSrc = "./blazor.boot.json"; } @@ -468,9 +474,7 @@ async function createEmscriptenMain(): Promise { const promises = importModules(); - await initCacheToUseIfEnabled(); - - const wasmModuleAsset = resolve_single_asset_path("dotnetwasm"); + const wasmModuleAsset = resolve_asset_path("dotnetwasm"); start_asset_download(wasmModuleAsset).then(asset => { loaderHelpers.wasmDownloadPromise.promise_control.resolve(asset); }); @@ -483,7 +487,7 @@ async function createEmscriptenMain(): Promise { await runtimeHelpers.dotnetReady.promise; - await invokeLibraryInitializers("onRuntimeReady", [globalObjectsRoot.api], "modulesAfterRuntimeReady"); + await invokeLibraryInitializers("onRuntimeReady", [globalObjectsRoot.api], "onRuntimeReady"); return exportedRuntimeAPI; } diff --git a/src/mono/wasm/runtime/satelliteAssemblies.ts b/src/mono/wasm/runtime/satelliteAssemblies.ts index 100af0696aae4d..612d757efe8591 100644 --- a/src/mono/wasm/runtime/satelliteAssemblies.ts +++ b/src/mono/wasm/runtime/satelliteAssemblies.ts @@ -1,35 +1,24 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -import { loaderHelpers, runtimeHelpers } from "./globals"; -import { AssetEntry } from "./types"; +import { INTERNAL, loaderHelpers, runtimeHelpers } from "./globals"; +import type { WebAssemblyResourceLoader } from "./loader/blazor/WebAssemblyResourceLoader"; +import { LoadingResource } from "./types"; export async function loadSatelliteAssemblies(culturesToLoad: string[]): Promise { - const satelliteResources = loaderHelpers.config.resources!.satelliteResources; + const resourceLoader: WebAssemblyResourceLoader = INTERNAL.resourceLoader; + const satelliteResources = resourceLoader.bootConfig.resources.satelliteResources; if (!satelliteResources) { return; } await Promise.all(culturesToLoad! .filter(culture => Object.prototype.hasOwnProperty.call(satelliteResources, culture)) - .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; + .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(); 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 23bf16e7086f67..6c206b92489466 100644 --- a/src/mono/wasm/runtime/snapshot.ts +++ b/src/mono/wasm/runtime/snapshot.ts @@ -142,9 +142,22 @@ 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 @@ -160,6 +173,7 @@ 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 270f18354c9263..be65c3955d250e 100644 --- a/src/mono/wasm/runtime/startup.ts +++ b/src/mono/wasm/runtime/startup.ts @@ -269,10 +269,12 @@ async function onRuntimeInitializedAsync(userOnRuntimeInitialized: () => void) { if (!runtimeHelpers.mono_wasm_runtime_is_ready) mono_wasm_runtime_ready(); - if (loaderHelpers.config.debugLevel !== 0 && loaderHelpers.config.cacheBootResources) { - loaderHelpers.logDownloadStatsToConsole(); + 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 } - 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 new file mode 100644 index 00000000000000..205bac41d7e190 --- /dev/null +++ b/src/mono/wasm/runtime/types/blazor.ts @@ -0,0 +1,58 @@ +// 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 af76f00538f0dd..6222355fddb4cf 100644 --- a/src/mono/wasm/runtime/types/index.ts +++ b/src/mono/wasm/runtime/types/index.ts @@ -28,6 +28,14 @@ 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. */ @@ -58,11 +66,6 @@ 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 */ @@ -81,6 +84,10 @@ export type MonoConfig = { * If true, the snapshot of runtime's memory will be stored in the browser and used for faster startup next time. Default is false. */ startupMemoryCache?: boolean, + /** + * hash of assets + */ + assetsHash?: string, /** * application environment */ @@ -96,11 +103,6 @@ export type MonoConfig = { */ resources?: ResourceGroups; - /** - * appsettings files to load to VFS - */ - appsettings?: string[]; - /** * config extensions declared in MSBuild items @(WasmBootConfigExtension) */ @@ -110,31 +112,22 @@ export type MonoConfig = { export type ResourceExtensions = { [extensionName: string]: ResourceList }; export interface ResourceGroups { - 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 }; + 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 }; } -/** - * A "key" is name of the file, a "value" is optional hash for integrity check. - */ -export type ResourceList = { [name: string]: string | null | "" }; +export type ResourceList = { [name: string]: string }; /** * Overrides the built-in boot resource loading mechanism so that boot resources can be fetched @@ -145,13 +138,13 @@ export type ResourceList = { [name: string]: string | null | "" }; * @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: AssetBehaviors | "manifest", name: string, defaultUri: string, integrity: string) => string | Promise | null | undefined; +export type LoadBootResourceCallback = (type: WebAssemblyBootResourceType, name: string, defaultUri: string, integrity: string) => string | Promise | null | undefined; export interface ResourceRequest { name: string, // the name of the asset, including extension. - behavior: AssetBehaviors, // determines how the asset will be handled once loaded + behavior: AssetBehaviours, // determines how the asset will be handled once loaded resolvedUrl?: string; // this should be absolute url to the asset - hash?: string | null | ""; // the integrity hash of the asset (if any) + hash?: string; } export interface LoadingResource { @@ -190,25 +183,7 @@ export interface AssetEntry extends ResourceRequest { pendingDownload?: LoadingResource } -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 | +export type AssetBehaviours = /** * Load asset as a managed resource assembly. */ @@ -233,6 +208,26 @@ export type AssetBehaviors = SingleAssetBehaviors | * Load asset into the virtual filesystem (for fopen, File.Open, etc). */ | "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 . */ @@ -240,13 +235,13 @@ export type AssetBehaviors = SingleAssetBehaviors | /** * The javascript module for threads. */ - | "symbols" + | "symbols" // export const enum GlobalizationMode { /** * Load sharded ICU data. */ - Sharded = "sharded", + Sharded = "sharded", // /** * Load all ICU data. */ @@ -273,9 +268,11 @@ 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 = { @@ -349,6 +346,29 @@ 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 dda104d22d7218..66e7fb4c9bcf69 100644 --- a/src/mono/wasm/runtime/types/internal.ts +++ b/src/mono/wasm/runtime/types/internal.ts @@ -1,7 +1,8 @@ // 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, AssetEntry, DotnetModuleConfig, LoadBootResourceCallback, LoadingResource, MonoConfig, RuntimeAPI } from "."; +import type { AssetBehaviours, AssetEntry, DotnetModuleConfig, LoadBootResourceCallback, LoadingResource, MonoConfig, ResourceRequest, RuntimeAPI } from "."; +import type { BootJsonData } from "./blazor"; import type { CharPtr, EmscriptenModule, ManagedPointer, NativePointer, VoidPtr, Int32Ptr } from "./emscripten"; export type GCHandle = { @@ -68,8 +69,6 @@ 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. @@ -134,18 +133,16 @@ export type LoaderHelpers = { getPromiseController: (promise: ControllablePromise) => PromiseController, assertIsControllablePromise: (promise: Promise) => asserts promise is ControllablePromise, mono_download_assets: () => Promise, - resolve_asset_path: (behavior: AssetBehaviors) => AssetEntryInternal, + resolve_asset_path: (behavior: AssetBehaviours) => 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(config: MonoConfig): boolean, - retrieve_asset_download(asset: AssetEntry): Promise; - onDownloadResourceProgress?: (resourcesLoaded: number, totalResources: number) => void; - logDownloadStatsToConsole: () => void; - purgeUnusedCacheEntriesAsync: () => Promise; + hasDebuggingEnabled(bootConfig: BootJsonData): boolean, 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 deleted file mode 100644 index 92aa8531f1efb3..00000000000000 --- a/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/BootJsonBuilderHelper.cs +++ /dev/null @@ -1,77 +0,0 @@ -// 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 7bb1930c6f1190..81943d6ce83b00 100644 --- a/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/BootJsonData.cs +++ b/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/BootJsonData.cs @@ -17,13 +17,8 @@ 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, @@ -39,12 +34,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. @@ -54,33 +49,17 @@ 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. /// - /// - /// 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; } + public ICUDataMode icuDataMode { get; set; } /// /// Gets or sets a value that determines if the caching startup memory is enabled. @@ -123,29 +102,7 @@ public class ResourcesData /// /// .NET Wasm runtime resources (dotnet.wasm, dotnet.js) etc. /// - /// - /// 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; } + public ResourceHashesByNameDictionary runtime { get; set; } = new ResourceHashesByNameDictionary(); /// /// "assembly" (.dll) resources @@ -177,11 +134,12 @@ 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 ResourceHashesByNameDictionary modulesAfterConfigLoaded { get; set; } - - [DataMember(EmitDefaultValue = false)] - public ResourceHashesByNameDictionary modulesAfterRuntimeReady { get; set; } + public TypedLibraryStartupModules libraryStartupModules { get; set; } /// /// Extensions created by users customizing the initialization process. The format of the file(s) @@ -203,10 +161,19 @@ public class ResourcesData public List remoteSources { get; set; } } -public enum GlobalizationMode : int +[DataContract] +public class TypedLibraryStartupModules +{ + [DataMember(EmitDefaultValue = false)] + public ResourceHashesByNameDictionary onRuntimeConfigLoaded { get; set; } + + [DataMember(EmitDefaultValue = false)] + public ResourceHashesByNameDictionary onRuntimeReady { get; set; } +} + +public enum ICUDataMode : 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 dfa897ca3bbcff..a423251b4cb584 100644 --- a/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/GenerateWasmBootJson.cs +++ b/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/GenerateWasmBootJson.cs @@ -10,10 +10,6 @@ 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; @@ -62,9 +58,9 @@ public class GenerateWasmBootJson : Task [Required] public string TargetFrameworkVersion { get; set; } - public ITaskItem[] ModuleAfterConfigLoaded { get; set; } + public ITaskItem[] LibraryInitializerOnRuntimeConfigLoaded { get; set; } - public ITaskItem[] ModuleAfterRuntimeReady { get; set; } + public ITaskItem[] LibraryInitializerOnRuntimeReady { get; set; } [Required] public string OutputPath { get; set; } @@ -91,36 +87,19 @@ public override bool Execute() // 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(' '); @@ -148,8 +127,8 @@ public void WriteBootJson(Stream output, string entryAssemblyName) result.runtimeOptions = runtimeOptions.ToArray(); } - string[] moduleAfterConfigLoadedFullPaths = ModuleAfterConfigLoaded?.Select(s => s.GetMetadata("FullPath")).ToArray() ?? Array.Empty(); - string[] moduleAfterRuntimeReadyFullPaths = ModuleAfterRuntimeReady?.Select(s => s.GetMetadata("FullPath")).ToArray() ?? Array.Empty(); + string[] libraryInitializerOnRuntimeConfigLoadedFullPaths = LibraryInitializerOnRuntimeConfigLoaded?.Select(s => s.GetMetadata("FullPath")).ToArray() ?? Array.Empty(); + string[] libraryInitializerOnRuntimeReadyFullPath = LibraryInitializerOnRuntimeReady?.Select(s => s.GetMetadata("FullPath")).ToArray() ?? Array.Empty(); // Build a two-level dictionary of the form: // - assembly: @@ -184,9 +163,7 @@ public void WriteBootJson(Stream output, string entryAssemblyName) { Log.LogMessage(MessageImportance.Low, "Candidate '{0}' is defined as satellite assembly with culture '{1}'.", resource.ItemSpec, assetTraitValue); resourceData.satelliteResources ??= new Dictionary(StringComparer.OrdinalIgnoreCase); - - if (!IsTargeting80OrLater()) - resourceName = assetTraitValue + "/" + resourceName; + resourceName = assetTraitValue + "/" + resourceName; if (!resourceData.satelliteResources.TryGetValue(assetTraitValue, out resourceList)) { @@ -218,20 +195,12 @@ public void WriteBootJson(Stream output, string entryAssemblyName) string.Equals(assetTraitValue, "native", StringComparison.OrdinalIgnoreCase)) { Log.LogMessage(MessageImportance.Low, "Candidate '{0}' is defined as a native application resource.", resource.ItemSpec); - - if (IsTargeting80OrLater()) + if (fileName.StartsWith("dotnet", StringComparison.OrdinalIgnoreCase) && string.Equals(fileExtension, ".wasm", StringComparison.OrdinalIgnoreCase)) { - resourceList = helper.GetNativeResourceTargetInBootConfig(result, resourceName); + behavior = "dotnetwasm"; } - else - { - if (fileName.StartsWith("dotnet", StringComparison.OrdinalIgnoreCase) && string.Equals(fileExtension, ".wasm", StringComparison.OrdinalIgnoreCase)) - { - behavior = "dotnetwasm"; - } - resourceList = resourceData.runtime ??= new(); - } + resourceList = resourceData.runtime; } else if (string.Equals("JSModule", assetTraitName, StringComparison.OrdinalIgnoreCase) && string.Equals(assetTraitValue, "JSLibraryModule", StringComparison.OrdinalIgnoreCase)) @@ -246,25 +215,27 @@ public void WriteBootJson(Stream output, string entryAssemblyName) if (IsTargeting80OrLater()) { - if (moduleAfterConfigLoadedFullPaths.Contains(resource.ItemSpec)) + var libraryStartupModules = resourceData.libraryStartupModules ??= new TypedLibraryStartupModules(); + + if (libraryInitializerOnRuntimeConfigLoadedFullPaths.Contains(resource.ItemSpec)) { - resourceList = resourceData.modulesAfterConfigLoaded ??= new(); + resourceList = libraryStartupModules.onRuntimeConfigLoaded ??= new(); } - else if (moduleAfterRuntimeReadyFullPaths.Contains(resource.ItemSpec)) + else if (libraryInitializerOnRuntimeReadyFullPath.Contains(resource.ItemSpec)) { - resourceList = resourceData.modulesAfterRuntimeReady ??= new(); + resourceList = libraryStartupModules.onRuntimeReady ??= new(); } else if (File.Exists(resource.ItemSpec)) { string fileContent = File.ReadAllText(resource.ItemSpec); if (fileContent.Contains("onRuntimeConfigLoaded") || fileContent.Contains("beforeStart") || fileContent.Contains("afterStarted")) - resourceList = resourceData.modulesAfterConfigLoaded ??= new(); + resourceList = libraryStartupModules.onRuntimeConfigLoaded ??= new(); else - resourceList = resourceData.modulesAfterRuntimeReady ??= new(); + resourceList = libraryStartupModules.onRuntimeReady ??= new(); } else { - resourceList = resourceData.modulesAfterConfigLoaded ??= new(); + resourceList = libraryStartupModules.onRuntimeConfigLoaded ??= new(); } string newTargetPath = "../" + targetPath; // This needs condition once WasmRuntimeAssetsLocation is supported in Wasm SDK @@ -335,40 +306,38 @@ public void WriteBootJson(Stream output, string entryAssemblyName) { string configUrl = Path.GetFileName(configFile.ItemSpec); if (IsTargeting80OrLater()) - { - result.appsettings ??= new(); - configUrl = "../" + configUrl; // This needs condition once WasmRuntimeAssetsLocation is supported in Wasm SDK - result.appsettings.Add(configUrl); - } - else - { - result.config.Add(configUrl); - } + + result.config.Add(configUrl); } } - var jsonOptions = new JsonSerializerOptions() - { - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, - WriteIndented = true - }; - if (Extensions != null && Extensions.Length > 0) { + var configSerializer = new DataContractJsonSerializer(typeof(Dictionary), new DataContractJsonSerializerSettings + { + UseSimpleDictionaryFormat = true + }); + result.extensions = new Dictionary>(); foreach (var configExtension in Extensions) { var key = configExtension.GetMetadata("key"); using var fs = File.OpenRead(configExtension.ItemSpec); - var config = JsonSerializer.Deserialize>(fs, jsonOptions); + var config = (Dictionary)configSerializer.ReadObject(fs); result.extensions[key] = config; } } - helper.ComputeResourcesHash(result); - JsonSerializer.Serialize(output, result, jsonOptions); + 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); void AddResourceToList(ITaskItem resource, ResourceHashesByNameDictionary resourceList, string resourceKey) { @@ -380,18 +349,18 @@ void AddResourceToList(ITaskItem resource, ResourceHashesByNameDictionary resour } } - private GlobalizationMode GetGlobalizationMode() + private ICUDataMode GetIcuDataMode() { if (string.Equals(InvariantGlobalization, "true", StringComparison.OrdinalIgnoreCase)) - return GlobalizationMode.Invariant; + return ICUDataMode.Invariant; else if (IsHybridGlobalization) - return GlobalizationMode.Hybrid; + return ICUDataMode.Hybrid; else if (LoadAllICUData) - return GlobalizationMode.All; + return ICUDataMode.All; else if (LoadCustomIcuData) - return GlobalizationMode.Custom; + return ICUDataMode.Custom; - return GlobalizationMode.Sharded; + return ICUDataMode.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 2f6ef98d6ad48c..747e216bfb46e2 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,7 +20,6 @@ - diff --git a/src/tasks/WasmAppBuilder/WasmAppBuilder.cs b/src/tasks/WasmAppBuilder/WasmAppBuilder.cs index 9c7d8a6799134e..12e7cb3a581c78 100644 --- a/src/tasks/WasmAppBuilder/WasmAppBuilder.cs +++ b/src/tasks/WasmAppBuilder/WasmAppBuilder.cs @@ -8,7 +8,6 @@ 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; @@ -59,32 +58,30 @@ protected override bool ValidateArguments() return true; } - private GlobalizationMode GetGlobalizationMode() + private ICUDataMode GetICUDataMode() { // Invariant has always precedence if (InvariantGlobalization) - return GlobalizationMode.Invariant; + return ICUDataMode.Invariant; // If user provided a path to a custom ICU data file, use it if (!string.IsNullOrEmpty(WasmIcuDataFileName)) - return GlobalizationMode.Custom; + return ICUDataMode.Custom; // Hybrid mode if (HybridGlobalization) - return GlobalizationMode.Hybrid; + return ICUDataMode.Hybrid; // If user requested to include full ICU data, use it if (WasmIncludeFullIcuData) - return GlobalizationMode.All; + return ICUDataMode.All; // Otherwise, use sharded mode - return GlobalizationMode.Sharded; + return ICUDataMode.Sharded; } protected override bool ExecuteInternal() { - var helper = new BootJsonBuilderHelper(Log); - if (!ValidateArguments()) return false; @@ -98,8 +95,9 @@ protected override bool ExecuteInternal() var bootConfig = new BootJsonData() { - mainAssemblyName = MainAssemblyName, - globalizationMode = GetGlobalizationMode().ToString().ToLowerInvariant() + config = new(), + entryAssembly = MainAssemblyName, + icuDataMode = GetICUDataMode() }; // Create app @@ -160,9 +158,19 @@ protected override bool ExecuteInternal() var itemHash = Utils.ComputeIntegrity(item.ItemSpec); - Dictionary? resourceList = helper.GetNativeResourceTargetInBootConfig(bootConfig, name); - if (resourceList != null) - resourceList[name] = itemHash; + 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; } string packageJsonPath = Path.Combine(AppDir, "package.json"); @@ -205,6 +213,7 @@ protected override bool ExecuteInternal() } } + bootConfig.debugBuild = DebugLevel > 0; bootConfig.debugLevel = DebugLevel; ProcessSatelliteAssemblies(args => @@ -303,8 +312,7 @@ protected override bool ExecuteInternal() return false; } - bootConfig.resources.icu ??= new(); - bootConfig.resources.icu[Path.GetFileName(idfn)] = Utils.ComputeIntegrity(idfn); + bootConfig.resources.runtime[Path.GetFileName(idfn)] = Utils.ComputeIntegrity(idfn); } } @@ -362,15 +370,35 @@ protected override bool ExecuteInternal() string tmpMonoConfigPath = Path.GetTempFileName(); using (var sw = File.CreateText(tmpMonoConfigPath)) { - helper.ComputeResourcesHash(bootConfig); + var sb = new StringBuilder(); - var jsonOptions = new JsonSerializerOptions + static void AddDictionary(StringBuilder sb, Dictionary res) { - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, - WriteIndented = true - }; - var json = JsonSerializer.Serialize(bootConfig, jsonOptions); + 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 }); sw.Write(json); } diff --git a/src/tasks/WasmAppBuilder/WasmAppBuilder.csproj b/src/tasks/WasmAppBuilder/WasmAppBuilder.csproj index 1334684eb5dbae..2f47edac7f88e4 100644 --- a/src/tasks/WasmAppBuilder/WasmAppBuilder.csproj +++ b/src/tasks/WasmAppBuilder/WasmAppBuilder.csproj @@ -20,7 +20,6 @@ -