diff --git a/src/libraries/sendtohelix-browser.targets b/src/libraries/sendtohelix-browser.targets index c1e5d9d5d20c4f..eba7ec0e95f2c7 100644 --- a/src/libraries/sendtohelix-browser.targets +++ b/src/libraries/sendtohelix-browser.targets @@ -138,6 +138,7 @@ <_XUnitTraitArg Condition="'$(TestUsingWorkloads)' == 'true'">-notrait category=no-workload <_XUnitTraitArg Condition="'$(TestUsingWorkloads)' != 'true'">-trait category=no-workload <_XUnitTraitArg Condition="'$(WasmFingerprintAssets)' == 'false'">$(_XUnitTraitArg) -trait category=no-fingerprinting + <_XUnitTraitArg Condition="'$(WasmBundlerFriendlyBootConfig)' == 'true'">$(_XUnitTraitArg) -trait category=bundler-friendly diff --git a/src/libraries/sendtohelix-wasm.targets b/src/libraries/sendtohelix-wasm.targets index f92b17d4fc9723..3a8b618f54f3d9 100644 --- a/src/libraries/sendtohelix-wasm.targets +++ b/src/libraries/sendtohelix-wasm.targets @@ -14,6 +14,7 @@ NoWorkload- NoWebcil- NoFingerprint- + JavascriptBundler- $(WorkItemPrefix)ST- $(WorkItemPrefix)MT- @@ -50,7 +51,7 @@ - + $(_BuildWasmAppsPayloadArchive) set "HELIX_XUNIT_ARGS=-class %(Identity)" export "HELIX_XUNIT_ARGS=-class %(Identity)" @@ -58,7 +59,7 @@ $(_workItemTimeout) - + $(_BuildWasmAppsPayloadArchive) $(HelixCommand) $(_workItemTimeout) diff --git a/src/libraries/sendtohelix.proj b/src/libraries/sendtohelix.proj index a916ea3e836cc3..c121c3c009d51b 100644 --- a/src/libraries/sendtohelix.proj +++ b/src/libraries/sendtohelix.proj @@ -88,10 +88,12 @@ <_TestUsingWorkloadsValues Include="true;false" /> <_TestUsingWebcilValues Include="true;false" Condition="'$(TargetOS)' == 'browser'" /> <_TestUsingFingerprintingValues Include="true;false" Condition="'$(TargetOS)' == 'browser'" /> + <_TestUsingJavascriptBundlerValues Include="true;false" Condition="'$(TargetOS)' == 'browser'" /> <_TestUsingCrossProductValuesTemp Include="@(_TestUsingWorkloadsValues)"> %(_TestUsingWorkloadsValues.Identity) + false <_TestUsingCrossProductValuesTemp2 Include="@(_TestUsingCrossProductValuesTemp)"> %(_TestUsingWebcilValues.Identity) @@ -102,9 +104,24 @@ <_TestUsingCrossProductValues Remove="@(_TestUsingCrossProductValues)" Condition="'%(_TestUsingCrossProductValues.Workloads)' == 'false' and '%(_TestUsingCrossProductValues.Fingerprinting)' == 'false'" /> + + + <_TestUsingCrossProductValues Include="JavaScriptBundlerFriendly"> + true + true + true + true + <_BuildWasmAppsProjectsToBuild Include="$(PerScenarioProjectFile)"> - $(_PropertiesToPass);Scenario=BuildWasmApps;TestArchiveRuntimeFile=$(TestArchiveRuntimeFile);TestUsingWorkloads=%(_TestUsingCrossProductValues.Workloads);WasmEnableWebcil=%(_TestUsingCrossProductValues.Webcil);WasmFingerprintAssets=%(_TestUsingCrossProductValues.Fingerprinting) + + $(_PropertiesToPass);Scenario=BuildWasmApps; + TestArchiveRuntimeFile=$(TestArchiveRuntimeFile); + TestUsingWorkloads=%(_TestUsingCrossProductValues.Workloads); + WasmEnableWebcil=%(_TestUsingCrossProductValues.Webcil); + WasmFingerprintAssets=%(_TestUsingCrossProductValues.Fingerprinting); + WasmBundlerFriendlyBootConfig=%(_TestUsingCrossProductValues.BundlerFriendly) + %(_BuildWasmAppsProjectsToBuild.AdditionalProperties);NeedsToBuildWasmAppsOnHelix=$(NeedsToBuildWasmAppsOnHelix) diff --git a/src/libraries/sendtohelixhelp.proj b/src/libraries/sendtohelixhelp.proj index b01079ab6f07d1..87d3c1d371f808 100644 --- a/src/libraries/sendtohelixhelp.proj +++ b/src/libraries/sendtohelixhelp.proj @@ -159,6 +159,7 @@ + @@ -348,7 +349,7 @@ + Text="Scenario: $(Scenario), TestUsingWorkloads: $(TestUsingWorkloads), WasmEnableWebcil: $(WasmEnableWebcil), WasmFingerprintAssets: $(WasmFingerprintAssets), WasmBundlerFriendlyBootConfig: $(WasmBundlerFriendlyBootConfig)" /> diff --git a/src/mono/browser/runtime/dotnet.d.ts b/src/mono/browser/runtime/dotnet.d.ts index 5700961fd53581..7184ad364453d6 100644 --- a/src/mono/browser/runtime/dotnet.d.ts +++ b/src/mono/browser/runtime/dotnet.d.ts @@ -220,10 +220,7 @@ type MonoConfig = { * Gets the application culture. This is a name specified in the BCP 47 format. See https://tools.ietf.org/html/bcp47 */ applicationCulture?: string; - /** - * definition of assets to load along with the runtime. - */ - resources?: ResourceGroups; + resources?: Assets; /** * appsettings files to load to VFS */ @@ -247,36 +244,84 @@ type MonoConfig = { type ResourceExtensions = { [extensionName: string]: ResourceList; }; -interface ResourceGroups { +interface Assets { hash?: string; - fingerprinting?: { - [name: string]: string; - }; - coreAssembly?: ResourceList; - assembly?: ResourceList; - lazyAssembly?: ResourceList; - corePdb?: ResourceList; - pdb?: ResourceList; - jsModuleWorker?: ResourceList; - jsModuleDiagnostics?: ResourceList; - jsModuleNative: ResourceList; - jsModuleRuntime: ResourceList; - wasmSymbols?: ResourceList; - wasmNative: ResourceList; - icu?: ResourceList; + coreAssembly?: AssemblyAsset[]; + assembly?: AssemblyAsset[]; + lazyAssembly?: AssemblyAsset[]; + corePdb?: PdbAsset[]; + pdb?: PdbAsset[]; + jsModuleWorker?: JsAsset[]; + jsModuleDiagnostics?: JsAsset[]; + jsModuleNative: JsAsset[]; + jsModuleRuntime: JsAsset[]; + wasmSymbols?: SymbolsAsset[]; + wasmNative: WasmAsset[]; + icu?: IcuAsset[]; satelliteResources?: { - [cultureName: string]: ResourceList; + [cultureName: string]: AssemblyAsset[]; }; - modulesAfterConfigLoaded?: ResourceList; - modulesAfterRuntimeReady?: ResourceList; + modulesAfterConfigLoaded?: JsAsset[]; + modulesAfterRuntimeReady?: JsAsset[]; extensions?: ResourceExtensions; - coreVfs?: { - [virtualPath: string]: ResourceList; - }; - vfs?: { - [virtualPath: string]: ResourceList; - }; + coreVfs?: VfsAsset[]; + vfs?: VfsAsset[]; } +type Asset = { + /** + * this should be absolute url to the asset + */ + resolvedUrl?: string; + /** + * If true, the runtime startup would not fail if the asset download was not successful. + */ + isOptional?: boolean; + /** + * If provided, runtime doesn't have to fetch the data. + * Runtime would set the buffer to null after instantiation to free the memory. + */ + buffer?: ArrayBuffer | Promise; + /** + * It's metadata + fetch-like Promise + * If provided, the runtime doesn't have to initiate the download. It would just await the response. + */ + pendingDownload?: LoadingResource; +}; +type WasmAsset = Asset & { + name: string; + hash?: string | null | ""; +}; +type AssemblyAsset = Asset & { + virtualPath: string; + name: string; + hash?: string | null | ""; +}; +type PdbAsset = Asset & { + virtualPath: string; + name: string; + hash?: string | null | ""; +}; +type JsAsset = Asset & { + /** + * If provided, runtime doesn't have to import it's JavaScript modules. + * This will not work for multi-threaded runtime. + */ + moduleExports?: any | Promise; + name?: string; +}; +type SymbolsAsset = Asset & { + name: string; +}; +type VfsAsset = Asset & { + virtualPath: string; + name: string; + hash?: string | null | ""; +}; +type IcuAsset = Asset & { + virtualPath: string; + name: string; + hash?: string | null | ""; +}; /** * A "key" is name of the file, a "value" is optional hash for integrity check. */ diff --git a/src/mono/browser/runtime/lazyLoading.ts b/src/mono/browser/runtime/lazyLoading.ts index 9787c18ac33ec3..2643a4d6b5cb79 100644 --- a/src/mono/browser/runtime/lazyLoading.ts +++ b/src/mono/browser/runtime/lazyLoading.ts @@ -3,7 +3,8 @@ import { loaderHelpers } from "./globals"; import { load_lazy_assembly } from "./managed-exports"; -import { AssetEntry } from "./types"; +import { type AssemblyAsset, type PdbAsset } from "./types"; +import { type AssetEntryInternal } from "./types/internal"; export async function loadLazyAssembly (assemblyNameToLoad: string): Promise { const resources = loaderHelpers.config.resources!; @@ -20,50 +21,35 @@ export async function loadLazyAssembly (assemblyNameToLoad: string): Promise { - if (resources.fingerprinting && (asset.behavior == "assembly" || asset.behavior == "pdb" || asset.behavior == "resource")) { - asset.virtualPath = getNonFingerprintedAssetName(asset.name); - } + const addAsset = (asset: Asset, behavior: AssetBehaviors, isCore: boolean) => { + const assetEntry = asset as AssetEntryInternal; + assetEntry.behavior = behavior; if (isCore) { - asset.isCore = true; - coreAssetsToLoad.push(asset); + assetEntry.isCore = true; + coreAssetsToLoad.push(assetEntry); } else { - assetsToLoad.push(asset); + assetsToLoad.push(assetEntry); } }; if (resources.coreAssembly) { - for (const name in resources.coreAssembly) { - addAsset({ - name, - hash: resources.coreAssembly[name], - behavior: "assembly" - }, true); + for (let i = 0; i < resources.coreAssembly.length; i++) { + const asset = resources.coreAssembly[i]; + addAsset(asset, "assembly", true); } } if (resources.assembly) { - for (const name in resources.assembly) { - addAsset({ - name, - hash: resources.assembly[name], - behavior: "assembly" - }, !resources.coreAssembly); // if there are no core assemblies, then all assemblies are core + for (let i = 0; i < resources.assembly.length; i++) { + const asset = resources.assembly[i]; + addAsset(asset, "assembly", !resources.coreAssembly); } } if (config.debugLevel != 0 && loaderHelpers.isDebuggingSupported()) { if (resources.corePdb) { - for (const name in resources.corePdb) { - addAsset({ - name, - hash: resources.corePdb[name], - behavior: "pdb" - }, true); + for (let i = 0; i < resources.corePdb.length; i++) { + const asset = resources.corePdb[i]; + addAsset(asset, "pdb", true); } } if (resources.pdb) { - for (const name in resources.pdb) { - addAsset({ - name, - hash: resources.pdb[name], - behavior: "pdb" - }, !resources.corePdb); // if there are no core pdbs, then all pdbs are core + for (let i = 0; i < resources.pdb.length; i++) { + const asset = resources.pdb[i]; + addAsset(asset, "pdb", !resources.corePdb); } } } if (config.loadAllSatelliteResources && resources.satelliteResources) { for (const culture in resources.satelliteResources) { - for (const name in resources.satelliteResources[culture]) { - addAsset({ - name, - hash: resources.satelliteResources[culture][name], - behavior: "resource", - culture - }, !resources.coreAssembly); + for (let i = 0; i < resources.satelliteResources[culture].length; i++) { + const asset = resources.satelliteResources[culture][i] as AssemblyAsset & AssetEntryInternal; + asset.culture = culture; + addAsset(asset, "resource", !resources.coreAssembly); } } } if (resources.coreVfs) { - for (const virtualPath in resources.coreVfs) { - for (const name in resources.coreVfs[virtualPath]) { - addAsset({ - name, - hash: resources.coreVfs[virtualPath][name], - behavior: "vfs", - virtualPath - }, true); - } + for (let i = 0; i < resources.coreVfs.length; i++) { + const asset = resources.coreVfs[i]; + addAsset(asset, "vfs", true); } } if (resources.vfs) { - for (const virtualPath in resources.vfs) { - for (const name in resources.vfs[virtualPath]) { - addAsset({ - name, - hash: resources.vfs[virtualPath][name], - behavior: "vfs", - virtualPath - }, !resources.coreVfs); - } + for (let i = 0; i < resources.vfs.length; i++) { + const asset = resources.vfs[i]; + addAsset(asset, "vfs", !resources.coreVfs); } } const icuDataResourceName = getIcuResourceName(config); if (icuDataResourceName && resources.icu) { - for (const name in resources.icu) { - if (name === icuDataResourceName) { - assetsToLoad.push({ - name, - hash: resources.icu[name], - behavior: "icu", - loadRemote: true - }); + for (let i = 0; i < resources.icu.length; i++) { + const asset = resources.icu[i]; + if (asset.name === icuDataResourceName) { + addAsset(asset, "icu", false); } } } if (resources.wasmSymbols) { - for (const name in resources.wasmSymbols) { - coreAssetsToLoad.push({ - name, - hash: resources.wasmSymbols[name], - behavior: "symbols" - }); + for (let i = 0; i < resources.wasmSymbols.length; i++) { + const asset = resources.wasmSymbols[i]; + addAsset(asset, "symbols", false); } } } @@ -450,15 +410,6 @@ export function prepareAssets () { config.assets = [...coreAssetsToLoad, ...assetsToLoad, ...modulesAssets]; } -export function getNonFingerprintedAssetName (assetName: string) { - const fingerprinting = loaderHelpers.config.resources?.fingerprinting; - if (fingerprinting && fingerprinting[assetName]) { - return fingerprinting[assetName]; - } - - return assetName; -} - export function prepareAssetsWorker () { const config = loaderHelpers.config; mono_assert(config.assets, "config.assets must be defined"); diff --git a/src/mono/browser/runtime/loader/config.ts b/src/mono/browser/runtime/loader/config.ts index b0cb66ad8d47d8..f882b2b1e192ea 100644 --- a/src/mono/browser/runtime/loader/config.ts +++ b/src/mono/browser/runtime/loader/config.ts @@ -5,7 +5,7 @@ import BuildConfiguration from "consts:configuration"; import WasmEnableThreads from "consts:wasmEnableThreads"; import { type DotnetModuleInternal, type MonoConfigInternal, JSThreadBlockingMode } from "../types/internal"; -import type { BootModule, DotnetModuleConfig, MonoConfig, ResourceGroups, ResourceList } from "../types"; +import type { AssemblyAsset, Assets, BootModule, DotnetModuleConfig, IcuAsset, JsAsset, MonoConfig, PdbAsset, SymbolsAsset, VfsAsset, WasmAsset } from "../types"; import { exportedRuntimeAPI, loaderHelpers, runtimeHelpers } from "./globals"; import { mono_log_error, mono_log_debug } from "./logging"; import { importLibraryInitializers, invokeLibraryInitializers } from "./libraryInitializers"; @@ -24,10 +24,10 @@ export function deep_merge_config (target: MonoConfigInternal, source: MonoConfi } if (providedConfig.resources !== undefined) { providedConfig.resources = deep_merge_resources(target.resources || { - assembly: {}, - jsModuleNative: {}, - jsModuleRuntime: {}, - wasmNative: {} + assembly: [], + jsModuleNative: [], + jsModuleRuntime: [], + wasmNative: [] }, providedConfig.resources); } if (providedConfig.environmentVariables !== undefined) { @@ -51,65 +51,65 @@ export function deep_merge_module (target: DotnetModuleInternal, source: DotnetM return Object.assign(target, providedConfig); } -function deep_merge_resources (target: ResourceGroups, source: ResourceGroups): ResourceGroups { +function deep_merge_resources (target: Assets, source: Assets): Assets { // no need to merge the same object if (target === source) return target; - const providedResources: ResourceGroups = { ...source }; + const providedResources: Assets = { ...source }; if (providedResources.assembly !== undefined) { - providedResources.assembly = { ...(target.assembly || {}), ...(providedResources.assembly || {}) }; + providedResources.assembly = [...(target.assembly || []), ...(providedResources.assembly || [])]; } if (providedResources.lazyAssembly !== undefined) { - providedResources.lazyAssembly = { ...(target.lazyAssembly || {}), ...(providedResources.lazyAssembly || {}) }; + providedResources.lazyAssembly = [...(target.lazyAssembly || []), ...(providedResources.lazyAssembly || [])]; } if (providedResources.pdb !== undefined) { - providedResources.pdb = { ...(target.pdb || {}), ...(providedResources.pdb || {}) }; + providedResources.pdb = [...(target.pdb || []), ...(providedResources.pdb || [])]; } if (providedResources.jsModuleWorker !== undefined) { - providedResources.jsModuleWorker = { ...(target.jsModuleWorker || {}), ...(providedResources.jsModuleWorker || {}) }; + providedResources.jsModuleWorker = [...(target.jsModuleWorker || []), ...(providedResources.jsModuleWorker || [])]; } if (providedResources.jsModuleNative !== undefined) { - providedResources.jsModuleNative = { ...(target.jsModuleNative || {}), ...(providedResources.jsModuleNative || {}) }; + providedResources.jsModuleNative = [...(target.jsModuleNative || []), ...(providedResources.jsModuleNative || [])]; } if (providedResources.jsModuleDiagnostics !== undefined) { - providedResources.jsModuleDiagnostics = { ...(target.jsModuleDiagnostics || {}), ...(providedResources.jsModuleDiagnostics || {}) }; + providedResources.jsModuleDiagnostics = [...(target.jsModuleDiagnostics || []), ...(providedResources.jsModuleDiagnostics || [])]; } if (providedResources.jsModuleRuntime !== undefined) { - providedResources.jsModuleRuntime = { ...(target.jsModuleRuntime || {}), ...(providedResources.jsModuleRuntime || {}) }; + providedResources.jsModuleRuntime = [...(target.jsModuleRuntime || []), ...(providedResources.jsModuleRuntime || [])]; } if (providedResources.wasmSymbols !== undefined) { - providedResources.wasmSymbols = { ...(target.wasmSymbols || {}), ...(providedResources.wasmSymbols || {}) }; + providedResources.wasmSymbols = [...(target.wasmSymbols || []), ...(providedResources.wasmSymbols || [])]; } if (providedResources.wasmNative !== undefined) { - providedResources.wasmNative = { ...(target.wasmNative || {}), ...(providedResources.wasmNative || {}) }; + providedResources.wasmNative = [...(target.wasmNative || []), ...(providedResources.wasmNative || [])]; } if (providedResources.icu !== undefined) { - providedResources.icu = { ...(target.icu || {}), ...(providedResources.icu || {}) }; + providedResources.icu = [...(target.icu || []), ...(providedResources.icu || [])]; } if (providedResources.satelliteResources !== undefined) { - providedResources.satelliteResources = deep_merge_dict(target.satelliteResources || {}, providedResources.satelliteResources || {}); + providedResources.satelliteResources = deepMergeSatelliteResources(target.satelliteResources || {}, providedResources.satelliteResources || {}); } if (providedResources.modulesAfterConfigLoaded !== undefined) { - providedResources.modulesAfterConfigLoaded = { ...(target.modulesAfterConfigLoaded || {}), ...(providedResources.modulesAfterConfigLoaded || {}) }; + providedResources.modulesAfterConfigLoaded = [...(target.modulesAfterConfigLoaded || []), ...(providedResources.modulesAfterConfigLoaded || [])]; } if (providedResources.modulesAfterRuntimeReady !== undefined) { - providedResources.modulesAfterRuntimeReady = { ...(target.modulesAfterRuntimeReady || {}), ...(providedResources.modulesAfterRuntimeReady || {}) }; + providedResources.modulesAfterRuntimeReady = [...(target.modulesAfterRuntimeReady || []), ...(providedResources.modulesAfterRuntimeReady || [])]; } if (providedResources.extensions !== undefined) { providedResources.extensions = { ...(target.extensions || {}), ...(providedResources.extensions || {}) }; } if (providedResources.vfs !== undefined) { - providedResources.vfs = deep_merge_dict(target.vfs || {}, providedResources.vfs || {}); + providedResources.vfs = [...(target.vfs || []), ...(providedResources.vfs || [])]; } return Object.assign(target, providedResources); } -function deep_merge_dict (target: { [key: string]: ResourceList }, source: { [key: string]: ResourceList }) { +function deepMergeSatelliteResources (target: { [key: string]: AssemblyAsset[] }, source: { [key: string]: AssemblyAsset[] }) { // no need to merge the same object if (target === source) return target; for (const key in source) { - target[key] = { ...target[key], ...source[key] }; + target[key] = [...target[key] || [], ...source[key] || []]; } return target; } @@ -122,56 +122,53 @@ export function normalizeConfig () { config.environmentVariables = config.environmentVariables || {}; config.runtimeOptions = config.runtimeOptions || []; config.resources = config.resources || { - assembly: {}, - jsModuleNative: {}, - jsModuleWorker: {}, - jsModuleRuntime: {}, - wasmNative: {}, - vfs: {}, - satelliteResources: {}, + assembly: [], + jsModuleNative: [], + jsModuleWorker: [], + jsModuleRuntime: [], + wasmNative: [], + vfs: [], + satelliteResources: {} }; if (config.assets) { mono_log_debug("config.assets is deprecated, use config.resources instead"); for (const asset of config.assets) { - const resource = {} as ResourceList; - resource[asset.name] = asset.hash || ""; - const toMerge = {} as ResourceGroups; + const toMerge = {} as Assets; switch (asset.behavior as string) { case "assembly": - toMerge.assembly = resource; + toMerge.assembly = [asset as AssemblyAsset]; break; case "pdb": - toMerge.pdb = resource; + toMerge.pdb = [asset as PdbAsset]; break; case "resource": toMerge.satelliteResources = {}; - toMerge.satelliteResources[asset.culture!] = resource; + toMerge.satelliteResources[asset.culture!] = [asset as AssemblyAsset]; break; case "icu": - toMerge.icu = resource; + toMerge.icu = [asset as IcuAsset]; break; case "symbols": - toMerge.wasmSymbols = resource; + toMerge.wasmSymbols = [asset as SymbolsAsset]; break; case "vfs": - toMerge.vfs = {}; - toMerge.vfs[asset.virtualPath!] = resource; + toMerge.vfs = [asset as VfsAsset]; break; case "dotnetwasm": - toMerge.wasmNative = resource; + toMerge.wasmNative = [asset as WasmAsset]; break; case "js-module-threads": - toMerge.jsModuleWorker = resource; + toMerge.jsModuleWorker = [asset as JsAsset]; break; case "js-module-runtime": - toMerge.jsModuleRuntime = resource; + toMerge.jsModuleRuntime = [asset as JsAsset]; break; case "js-module-native": - toMerge.jsModuleNative = resource; + toMerge.jsModuleNative = [asset as JsAsset]; break; case "js-module-diagnostics": - toMerge.jsModuleDiagnostics = resource; + toMerge.jsModuleDiagnostics = [asset as JsAsset]; break; case "js-module-dotnet": // don't merge loader diff --git a/src/mono/browser/runtime/loader/icu.ts b/src/mono/browser/runtime/loader/icu.ts index 1e90674a67dcce..4e5b41bff13a24 100644 --- a/src/mono/browser/runtime/loader/icu.ts +++ b/src/mono/browser/runtime/loader/icu.ts @@ -5,7 +5,6 @@ import { mono_log_error } from "./logging"; import { GlobalizationMode, MonoConfig } from "../types"; import { ENVIRONMENT_IS_WEB, loaderHelpers } from "./globals"; import { mono_log_info, mono_log_debug } from "./logging"; -import { getNonFingerprintedAssetName } from "./assets"; export function init_globalization () { loaderHelpers.preferredIcuAsset = getIcuResourceName(loaderHelpers.config); @@ -48,24 +47,13 @@ export function getIcuResourceName (config: MonoConfig): string | null { // TODO: when starting on sidecar, we should pass default culture from UI thread const culture = config.applicationCulture || (ENVIRONMENT_IS_WEB ? (globalThis.navigator && globalThis.navigator.languages && globalThis.navigator.languages[0]) : Intl.DateTimeFormat().resolvedOptions().locale); - const icuFiles = Object.keys(config.resources.icu); - const fileMapping: { - [k: string]: string - } = {}; - for (let index = 0; index < icuFiles.length; index++) { - const icuFile = icuFiles[index]; - if (config.resources.fingerprinting) { - fileMapping[getNonFingerprintedAssetName(icuFile)] = icuFile; - } else { - fileMapping[icuFile] = icuFile; - } - } + const icuFiles = config.resources.icu; let icuFile = null; if (config.globalizationMode === GlobalizationMode.Custom) { // custom ICU file is saved in the resources with fingerprinting and does not require mapping if (icuFiles.length >= 1) { - return icuFiles[0]; + return icuFiles[0].name; } } else if (!culture || config.globalizationMode === GlobalizationMode.All) { icuFile = "icudt.dat"; @@ -73,8 +61,13 @@ export function getIcuResourceName (config: MonoConfig): string | null { icuFile = getShardedIcuResourceName(culture); } - if (icuFile && fileMapping[icuFile]) { - return fileMapping[icuFile]; + if (icuFile) { + for (let i = 0; i < icuFiles.length; i++) { + const asset = icuFiles[i]; + if (asset.virtualPath === icuFile) { + return asset.name; + } + } } } diff --git a/src/mono/browser/runtime/loader/libraryInitializers.ts b/src/mono/browser/runtime/loader/libraryInitializers.ts index 4dd0ccfed9ff25..d319d125b0b387 100644 --- a/src/mono/browser/runtime/loader/libraryInitializers.ts +++ b/src/mono/browser/runtime/loader/libraryInitializers.ts @@ -5,25 +5,26 @@ import { mono_log_debug, mono_log_warn } from "./logging"; import { appendUniqueQuery } from "./assets"; import { loaderHelpers } from "./globals"; import { mono_exit } from "./exit"; -import { ResourceList } from "../types"; +import { JsAsset } from "../types"; -export async function importLibraryInitializers (libraryInitializers: ResourceList | undefined): Promise { +export async function importLibraryInitializers (libraryInitializers: JsAsset[] | undefined): Promise { if (!libraryInitializers) { return; } - const initializerFiles = Object.keys(libraryInitializers); - await Promise.all(initializerFiles.map(f => importInitializer(f))); + await Promise.all((libraryInitializers ?? []).map(i => importInitializer(i!))); - async function importInitializer (path: string): Promise { + async function importInitializer (asset: JsAsset): Promise { try { - const adjustedPath = appendUniqueQuery(loaderHelpers.locateFile(path), "js-module-library-initializer"); - mono_log_debug(() => `Attempting to import '${adjustedPath}' for ${path}`); - const initializer = await import(/*! webpackIgnore: true */ adjustedPath); - - loaderHelpers.libraryInitializers!.push({ scriptName: path, exports: initializer }); + const path = asset.name!; + if (!asset.moduleExports) { + const adjustedPath = appendUniqueQuery(loaderHelpers.locateFile(path), "js-module-library-initializer"); + mono_log_debug(() => `Attempting to import '${adjustedPath}' for ${asset}`); + asset.moduleExports = await import(/*! webpackIgnore: true */ adjustedPath); + } + loaderHelpers.libraryInitializers!.push({ scriptName: path, exports: asset.moduleExports }); } catch (error) { - mono_log_warn(`Failed to import library initializer '${path}': ${error}`); + mono_log_warn(`Failed to import library initializer '${asset}': ${error}`); } } } diff --git a/src/mono/browser/runtime/rollup.config.js b/src/mono/browser/runtime/rollup.config.js index ec1de7d67b0f75..1966e3dfc7bb9f 100644 --- a/src/mono/browser/runtime/rollup.config.js +++ b/src/mono/browser/runtime/rollup.config.js @@ -171,6 +171,7 @@ const loaderConfig = { format: "es", file: nativeBinDir + "/dotnet.js", banner, + intro: "/*! bundlerFriendlyImports */", plugins, sourcemap: true, sourcemapPathTransform, diff --git a/src/mono/browser/runtime/satelliteAssemblies.ts b/src/mono/browser/runtime/satelliteAssemblies.ts index 713cfb94161f9b..ebbf6d88294bda 100644 --- a/src/mono/browser/runtime/satelliteAssemblies.ts +++ b/src/mono/browser/runtime/satelliteAssemblies.ts @@ -3,7 +3,8 @@ import { loaderHelpers } from "./globals"; import { load_satellite_assembly } from "./managed-exports"; -import { AssetEntry } from "./types"; +import type { AssemblyAsset } from "./types"; +import type { AssetEntryInternal } from "./types/internal"; export async function loadSatelliteAssemblies (culturesToLoad: string[]): Promise { const satelliteResources = loaderHelpers.config.resources!.satelliteResources; @@ -15,14 +16,10 @@ export async function loadSatelliteAssemblies (culturesToLoad: string[]): Promis .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 - }; - + for (let i = 0; i < satelliteResources[culture].length; i++) { + const asset = satelliteResources[culture][i] as AssemblyAsset & AssetEntryInternal; + asset.behavior = "resource"; + asset.culture = culture; promises.push(loaderHelpers.retrieve_asset_download(asset)); } diff --git a/src/mono/browser/runtime/types/index.ts b/src/mono/browser/runtime/types/index.ts index e521025b9a0133..74c487753f8cd2 100644 --- a/src/mono/browser/runtime/types/index.ts +++ b/src/mono/browser/runtime/types/index.ts @@ -174,10 +174,7 @@ export type MonoConfig = { */ applicationCulture?: string, - /** - * definition of assets to load along with the runtime. - */ - resources?: ResourceGroups; + resources?: Assets, /** * appsettings files to load to VFS @@ -203,31 +200,95 @@ export type MonoConfig = { export type ResourceExtensions = { [extensionName: string]: ResourceList }; -export interface ResourceGroups { +export interface Assets { hash?: string; - fingerprinting?: { [name: string]: string }, - coreAssembly?: ResourceList; // nullable only temporarily - assembly?: ResourceList; // nullable only temporarily - lazyAssembly?: ResourceList; // nullable only temporarily - corePdb?: ResourceList; - pdb?: ResourceList; - - jsModuleWorker?: ResourceList; - jsModuleDiagnostics?: ResourceList; - jsModuleNative: ResourceList; - jsModuleRuntime: ResourceList; - wasmSymbols?: ResourceList; - wasmNative: ResourceList; - icu?: ResourceList; - - satelliteResources?: { [cultureName: string]: ResourceList }; - - modulesAfterConfigLoaded?: ResourceList, - modulesAfterRuntimeReady?: ResourceList + coreAssembly?: AssemblyAsset[]; // nullable only temporarily + assembly?: AssemblyAsset[]; // nullable only temporarily + lazyAssembly?: AssemblyAsset[]; // nullable only temporarily + corePdb?: PdbAsset[]; + pdb?: PdbAsset[]; + + jsModuleWorker?: JsAsset[]; + jsModuleDiagnostics?: JsAsset[]; + jsModuleNative: JsAsset[]; + jsModuleRuntime: JsAsset[]; + + wasmSymbols?: SymbolsAsset[]; + wasmNative: WasmAsset[]; + icu?: IcuAsset[]; + + satelliteResources?: { [cultureName: string]: AssemblyAsset[] }; + + modulesAfterConfigLoaded?: JsAsset[], + modulesAfterRuntimeReady?: JsAsset[] extensions?: ResourceExtensions - coreVfs?: { [virtualPath: string]: ResourceList }; - vfs?: { [virtualPath: string]: ResourceList }; + coreVfs?: VfsAsset[]; + vfs?: VfsAsset[]; +} + +export type Asset = { + /** + * this should be absolute url to the asset + */ + resolvedUrl?: string; + /** + * If true, the runtime startup would not fail if the asset download was not successful. + */ + isOptional?: boolean + /** + * If provided, runtime doesn't have to fetch the data. + * Runtime would set the buffer to null after instantiation to free the memory. + */ + buffer?: ArrayBuffer | Promise, + /** + * It's metadata + fetch-like Promise + * If provided, the runtime doesn't have to initiate the download. It would just await the response. + */ + pendingDownload?: LoadingResource +} + +export type WasmAsset = Asset & { + name: string; + hash?: string | null | ""; +} + +export type AssemblyAsset = Asset & { + virtualPath: string; + name: string; // actually URL + hash?: string | null | ""; +} + +export type PdbAsset = Asset & { + virtualPath: string; + name: string; // actually URL + hash?: string | null | ""; +} + +export type JsAsset = Asset & { + /** + * If provided, runtime doesn't have to import it's JavaScript modules. + * This will not work for multi-threaded runtime. + */ + moduleExports?: any | Promise, + + name?: string; // actually URL +} + +export type SymbolsAsset = Asset & { + name: string; // actually URL +} + +export type VfsAsset = Asset & { + virtualPath: string; + name: string; // actually URL + hash?: string | null | ""; +} + +export type IcuAsset = Asset & { + virtualPath: string; + name: string; // actually URL + hash?: string | null | ""; } /** 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 55403335852af1..b934bf4260d58c 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 @@ -215,6 +215,9 @@ Copyright (c) .NET Foundation. All rights reserved. <_WasmFingerprintBootConfig Condition="'$(_WasmFingerprintBootConfig)' == ''">false <_WasmPreloadAssets>$(WasmPreloadAssets) <_WasmPreloadAssets Condition="'$(_WasmPreloadAssets)' == ''">true + + <_WasmBundlerFriendlyBootConfig>$(WasmBundlerFriendlyBootConfig) + <_WasmBundlerFriendlyBootConfig Condition="'$(_WasmBundlerFriendlyBootConfig)' == ''">false $(OutputPath)$(PublishDirName)\ @@ -412,7 +415,8 @@ Copyright (c) .NET Foundation. All rights reserved. IsPublish="false" IsAot="$(RunAOTCompilation)" IsMultiThreaded="$(WasmEnableThreads)" - FingerprintAssets="$(_WasmFingerprintAssets)" /> + FingerprintAssets="$(_WasmFingerprintAssets)" + BundlerFriendly="$(_WasmBundlerFriendlyBootConfig)" /> @@ -820,7 +824,8 @@ Copyright (c) .NET Foundation. All rights reserved. IsPublish="true" IsAot="$(RunAOTCompilation)" IsMultiThreaded="$(WasmEnableThreads)" - FingerprintAssets="$(_WasmFingerprintAssets)" /> + FingerprintAssets="$(_WasmFingerprintAssets)" + BundlerFriendly="$(_WasmBundlerFriendlyBootConfig)" /> diff --git a/src/mono/sample/wasm/Directory.Build.targets b/src/mono/sample/wasm/Directory.Build.targets index dea79fa3bb64cc..3d02ded4c2547e 100644 --- a/src/mono/sample/wasm/Directory.Build.targets +++ b/src/mono/sample/wasm/Directory.Build.targets @@ -83,7 +83,7 @@ - + diff --git a/src/mono/sample/wasm/browser-advanced/main.js b/src/mono/sample/wasm/browser-advanced/main.js index 6a22eee4a5c0a8..016a3771aa08c1 100644 --- a/src/mono/sample/wasm/browser-advanced/main.js +++ b/src/mono/sample/wasm/browser-advanced/main.js @@ -39,9 +39,9 @@ try { .withConfig({ maxParallelDownloads: 1, resources: { - modulesAfterConfigLoaded: { - "advanced-sample.lib.module.js": "" - } + modulesAfterConfigLoaded: [{ + "name": "advanced-sample.lib.module.js" + }] } }) .withModuleConfig({ diff --git a/src/mono/wasm/Wasm.Build.Tests/AppSettingsTests.cs b/src/mono/wasm/Wasm.Build.Tests/AppSettingsTests.cs index 3c2da858c29124..11b360a89eb032 100644 --- a/src/mono/wasm/Wasm.Build.Tests/AppSettingsTests.cs +++ b/src/mono/wasm/Wasm.Build.Tests/AppSettingsTests.cs @@ -24,23 +24,31 @@ public AppSettingsTests(ITestOutputHelper output, SharedBuildPerTestClassFixture public static IEnumerable LoadAppSettingsBasedOnApplicationEnvironmentData() { // Defaults - yield return new object?[] { false, null, null, "Development" }; + if (!EnvironmentVariables.UseJavascriptBundler) + yield return new object?[] { false, null, null, "Development" }; + yield return new object?[] { true, null, null, "Production" }; // Override defaults from MSBuild - yield return new object?[] { false, "Production", null, "Production" }; + if (!EnvironmentVariables.UseJavascriptBundler) + yield return new object?[] { false, "Production", null, "Production" }; + yield return new object?[] { true, "Development", null, "Development" }; // Override defaults from JavaScript - yield return new object?[] { false, null, "Production", "Production" }; + if (!EnvironmentVariables.UseJavascriptBundler) + yield return new object?[] { false, null, "Production", "Production" }; + yield return new object?[] { true, null, "Development", "Development" }; // Override MSBuild from JavaScript - yield return new object?[] { false, "FromMSBuild", "Production", "Production" }; + if (!EnvironmentVariables.UseJavascriptBundler) + yield return new object?[] { false, "FromMSBuild", "Production", "Production" }; + yield return new object?[] { true, "FromMSBuild", "Development", "Development" }; } - [Theory] + [Theory, TestCategory("bundler-friendly")] [MemberData(nameof(LoadAppSettingsBasedOnApplicationEnvironmentData))] public async Task LoadAppSettingsBasedOnApplicationEnvironment(bool publish, string? msBuildApplicationEnvironment, string? queryApplicationEnvironment, string expectedApplicationEnvironment) { diff --git a/src/mono/wasm/Wasm.Build.Tests/Blazor/MiscTests.cs b/src/mono/wasm/Wasm.Build.Tests/Blazor/MiscTests.cs index 2a7d9b58cae8cb..b8eaf4afcb4b0e 100644 --- a/src/mono/wasm/Wasm.Build.Tests/Blazor/MiscTests.cs +++ b/src/mono/wasm/Wasm.Build.Tests/Blazor/MiscTests.cs @@ -96,6 +96,6 @@ public void BugRegression_60479_WithRazorClassLib() string bootConfigPath = _provider.GetBootConfigPath(GetBlazorBinFrameworkDir(config, forPublish: true)); BootJsonData bootJson = _provider.GetBootJson(bootConfigPath); - Assert.Contains(bootJson.resources.lazyAssembly.Keys, f => f.StartsWith(razorClassLibraryName)); + Assert.Contains(((AssetsData)bootJson.resources).lazyAssembly, f => f.name.StartsWith(razorClassLibraryName)); } } diff --git a/src/mono/wasm/Wasm.Build.Tests/Common/BuildEnvironment.cs b/src/mono/wasm/Wasm.Build.Tests/Common/BuildEnvironment.cs index e21004e5aab04e..e1c3ed4cc023ca 100644 --- a/src/mono/wasm/Wasm.Build.Tests/Common/BuildEnvironment.cs +++ b/src/mono/wasm/Wasm.Build.Tests/Common/BuildEnvironment.cs @@ -32,7 +32,7 @@ public class BuildEnvironment public static readonly string RelativeTestAssetsPath = @"..\testassets\"; public static readonly string TestAssetsPath = Path.Combine(AppContext.BaseDirectory, "testassets"); public static readonly string TestDataPath = Path.Combine(AppContext.BaseDirectory, "data"); - public static readonly string TmpPath = Path.Combine(AppContext.BaseDirectory, "wbt artifacts"); + public static readonly string TmpPath = Path.Combine(AppContext.BaseDirectory, EnvironmentVariables.UseJavascriptBundler ? "wbtartifacts" : "wbt artifacts"); public static readonly string DefaultRuntimeIdentifier = #if TARGET_WASI diff --git a/src/mono/wasm/Wasm.Build.Tests/Common/EnvironmentVariables.cs b/src/mono/wasm/Wasm.Build.Tests/Common/EnvironmentVariables.cs index d928fc1e1b4069..c1a932aed44b92 100644 --- a/src/mono/wasm/Wasm.Build.Tests/Common/EnvironmentVariables.cs +++ b/src/mono/wasm/Wasm.Build.Tests/Common/EnvironmentVariables.cs @@ -23,6 +23,7 @@ internal static class EnvironmentVariables internal static readonly bool ShowBuildOutput = IsRunningOnCI || Environment.GetEnvironmentVariable("SHOW_BUILD_OUTPUT") is not null; internal static readonly bool UseWebcil = Environment.GetEnvironmentVariable("USE_WEBCIL_FOR_TESTS") is "true"; internal static readonly bool UseFingerprinting = Environment.GetEnvironmentVariable("USE_FINGERPRINTING_FOR_TESTS") is "true"; + internal static readonly bool UseJavascriptBundler = Environment.GetEnvironmentVariable("USE_JAVASCRIPT_BUNDLER_FOR_TESTS") is "true"; internal static readonly string? SdkDirName = Environment.GetEnvironmentVariable("SDK_DIR_NAME"); internal static readonly string? WasiSdkPath = Environment.GetEnvironmentVariable("WASI_SDK_PATH"); internal static readonly bool WorkloadsTestPreviousVersions = Environment.GetEnvironmentVariable("WORKLOADS_TEST_PREVIOUS_VERSIONS") is "true"; diff --git a/src/mono/wasm/Wasm.Build.Tests/LibraryInitializerTests.cs b/src/mono/wasm/Wasm.Build.Tests/LibraryInitializerTests.cs index b60bf5c858f31b..e281f86d48d8f4 100644 --- a/src/mono/wasm/Wasm.Build.Tests/LibraryInitializerTests.cs +++ b/src/mono/wasm/Wasm.Build.Tests/LibraryInitializerTests.cs @@ -24,7 +24,7 @@ public LibraryInitializerTests(ITestOutputHelper output, SharedBuildPerTestClass { } - [Fact] + [Fact, TestCategory("bundler-friendly")] public async Task LoadLibraryInitializer() { Configuration config = Configuration.Debug; @@ -40,7 +40,7 @@ public async Task LoadLibraryInitializer() [GeneratedRegex("MONO_WASM: Failed to invoke 'onRuntimeConfigLoaded' on library initializer '../WasmBasicTestApp.[a-z0-9]+.lib.module.js': Error: Error thrown from library initializer")] private static partial Regex AbortStartupOnErrorRegex(); - [Fact] + [Fact, TestCategory("bundler-friendly")] public async Task AbortStartupOnError() { Configuration config = Configuration.Debug; diff --git a/src/mono/wasm/Wasm.Build.Tests/ModuleConfigTests.cs b/src/mono/wasm/Wasm.Build.Tests/ModuleConfigTests.cs index 55548caee65e23..b50bedbe2b1aca 100644 --- a/src/mono/wasm/Wasm.Build.Tests/ModuleConfigTests.cs +++ b/src/mono/wasm/Wasm.Build.Tests/ModuleConfigTests.cs @@ -57,7 +57,7 @@ public async Task DownloadProgressFinishes(bool failAssemblyDownload) ); } - [Fact] + [Fact, TestCategory("bundler-friendly")] public async Task OutErrOverrideWorks() { Configuration config = Configuration.Debug; diff --git a/src/mono/wasm/Wasm.Build.Tests/ProjectProviderBase.cs b/src/mono/wasm/Wasm.Build.Tests/ProjectProviderBase.cs index 30dbee46735d21..e19d13914ea094 100644 --- a/src/mono/wasm/Wasm.Build.Tests/ProjectProviderBase.cs +++ b/src/mono/wasm/Wasm.Build.Tests/ProjectProviderBase.cs @@ -11,6 +11,7 @@ using System.Runtime.Serialization.Json; using System.Text; using System.Text.Json; +using System.Text.Json.Serialization; using System.Text.RegularExpressions; using Microsoft.NET.Sdk.WebAssembly; using Xunit; @@ -380,14 +381,23 @@ private string[] GetFilesMatchingNameConsideringFingerprinting(string filePath, { string bootJsonPath = GetBootConfigPath(paths.BinFrameworkDir, "dotnet.js"); BootJsonData bootJson = GetBootJson(bootJsonPath); + AssetsData assets = (AssetsData)bootJson.resources; var keysToUpdate = new List(); var updates = new List<(string oldKey, string newKey, (string fullPath, bool unchanged) value)>(); + List allAssemblies = [..assets.coreAssembly, ..assets.assembly]; + foreach (var expectedItem in dict) { string filename = Path.GetFileName(expectedItem.Value.fullPath); - var expectedFingerprintedItem = bootJson.resources.fingerprinting - .Where(kv => kv.Value == filename) - .SingleOrDefault().Key; + string? expectedFingerprintedItem = filename switch + { + "dotnet.runtime.js" => assets.jsModuleRuntime?.SingleOrDefault()?.name, + "dotnet.native.js" => assets.jsModuleNative?.SingleOrDefault()?.name, + "dotnet.native.wasm" => assets.wasmNative?.SingleOrDefault()?.name, + _ => filename == $"{projectName}{WasmAssemblyExtension}" + ? allAssemblies?.SingleOrDefault(a => a.virtualPath == $"{projectName}{WasmAssemblyExtension}")?.name + : null + }; if (string.IsNullOrEmpty(expectedFingerprintedItem)) continue; @@ -436,6 +446,8 @@ public static void AssertDotNetJsSymbols(AssertBundleOptions assertOptions) public void AssertIcuAssets(AssertBundleOptions assertOptions, BootJsonData bootJson) { + AssetsData assets = (AssetsData)bootJson.resources; + List expected = new(); switch (assertOptions.BuildOptions.GlobalizationMode) { @@ -468,7 +480,7 @@ public void AssertIcuAssets(AssertBundleOptions assertOptions, BootJsonData boot var expectedFingerprinted = new List(expected.Count); foreach (var expectedItem in expected) { - var expectedFingerprintedItem = bootJson.resources.fingerprinting.Where(kv => kv.Value == expectedItem).SingleOrDefault().Key; + var expectedFingerprintedItem = assets.icu.FirstOrDefault(a => a.virtualPath == expectedItem)?.name; if (string.IsNullOrEmpty(expectedFingerprintedItem)) throw new XunitException($"Could not find ICU asset {expectedItem} in fingerprinting in boot config"); @@ -540,30 +552,24 @@ public string GetBootConfigPath(string binFrameworkDir, string? bootConfigFileNa public BootJsonData AssertBootJson(AssertBundleOptions options) { + EnsureProjectDirIsSet(); string bootJsonPath = GetBootConfigPath(options.BinFrameworkDir, options.BuildOptions.BootConfigFileName); BootJsonData bootJson = GetBootJson(bootJsonPath); - string spcExpectedFilename = $"System.Private.CoreLib{WasmAssemblyExtension}"; + AssetsData assets = (AssetsData)bootJson.resources; - if (IsFingerprintingEnabled) - { - spcExpectedFilename = bootJson.resources.fingerprinting.Where(kv => kv.Value == spcExpectedFilename).SingleOrDefault().Key; - if (string.IsNullOrEmpty(spcExpectedFilename)) - throw new XunitException($"Could not find an assembly System.Private.CoreLib in fingerprinting in {bootJsonPath}"); - } + string spcExpectedFilename = $"System.Private.CoreLib{WasmAssemblyExtension}"; - string? spcActualFilename = bootJson.resources.coreAssembly.Keys - .Where(a => a == spcExpectedFilename) - .SingleOrDefault(); + string? spcActualFilename = assets.coreAssembly.SingleOrDefault(a => a.virtualPath == spcExpectedFilename)?.name; if (spcActualFilename is null) throw new XunitException($"Could not find an assembly named System.Private.CoreLib.* in {bootJsonPath}"); - var bootJsonEntries = bootJson.resources.jsModuleNative.Keys - .Union(bootJson.resources.wasmNative.Keys) - .Union(bootJson.resources.jsModuleRuntime.Keys) - .Union(bootJson.resources.jsModuleWorker?.Keys ?? Enumerable.Empty()) - .Union(bootJson.resources.jsModuleDiagnostics?.Keys ?? Enumerable.Empty()) - .Union(bootJson.resources.wasmSymbols?.Keys ?? Enumerable.Empty()) + var bootJsonEntries = assets.jsModuleNative.Select(a => a.name) + .Union(assets.wasmNative.Select(a => a.name)) + .Union(assets.jsModuleRuntime.Select(a => a.name)) + .Union(assets.jsModuleWorker?.Select(a => a.name) ?? Enumerable.Empty()) + .Union(assets.jsModuleDiagnostics?.Select(a => a.name) ?? Enumerable.Empty()) + .Union(assets.wasmSymbols?.Select(a => a.name) ?? Enumerable.Empty()) .ToArray(); var expectedEntries = new SortedDictionary>(); @@ -621,7 +627,9 @@ public static BootJsonData ParseBootData(string bootConfigPath) string jsonContent = GetBootJsonContent(bootConfigPath); try { - BootJsonData? config = JsonSerializer.Deserialize(jsonContent, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; + options.Converters.Add(new ResourcesConverter()); + BootJsonData? config = JsonSerializer.Deserialize(jsonContent, options); Assert.NotNull(config); return config!; } @@ -681,4 +689,33 @@ protected void EnsureProjectDirIsSet() if (string.IsNullOrEmpty(ProjectDir)) throw new Exception($"{nameof(ProjectDir)} is not set"); } + + internal class ResourcesConverter : JsonConverter + { + public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var nestedOptions = new JsonSerializerOptions(options); + nestedOptions.Converters.Remove(this); + + if (reader.TokenType == JsonTokenType.StartObject) + { + try + { + + return JsonSerializer.Deserialize(ref reader, nestedOptions)!; + } + catch + { + return JsonSerializer.Deserialize(ref reader, nestedOptions)!; + } + } + + return JsonSerializer.Deserialize(ref reader, nestedOptions)!; + } + + public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options) + { + JsonSerializer.Serialize(writer, value, value.GetType(), options); + } + } } diff --git a/src/mono/wasm/Wasm.Build.Tests/SatelliteLoadingTests.cs b/src/mono/wasm/Wasm.Build.Tests/SatelliteLoadingTests.cs index 98d40e6e9eefc9..90d27ad7287c76 100644 --- a/src/mono/wasm/Wasm.Build.Tests/SatelliteLoadingTests.cs +++ b/src/mono/wasm/Wasm.Build.Tests/SatelliteLoadingTests.cs @@ -58,7 +58,7 @@ public async Task LoadSatelliteAssembly(bool loadAllSatelliteResources) ); } - [Fact] + [Fact, TestCategory("bundler-friendly")] public async Task LoadSatelliteAssemblyFromReference() { Configuration config = Configuration.Release; diff --git a/src/mono/wasm/Wasm.Build.Tests/Templates/WasmTemplateTestsBase.cs b/src/mono/wasm/Wasm.Build.Tests/Templates/WasmTemplateTestsBase.cs index 5288275ce6a643..4d1b08ac1e1d6b 100644 --- a/src/mono/wasm/Wasm.Build.Tests/Templates/WasmTemplateTestsBase.cs +++ b/src/mono/wasm/Wasm.Build.Tests/Templates/WasmTemplateTestsBase.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Collections.Specialized; +using System.Configuration; using System.IO; using System.Linq; using System.Text; @@ -104,6 +105,17 @@ protected ProjectInfo CopyTestAsset( _projectDir = Path.Combine(_projectDir, asset.RunnableProjectSubPath); } string projectFilePath = Path.Combine(_projectDir, $"{asset.Name}.csproj"); + + if (EnvironmentVariables.UseJavascriptBundler) + { + extraProperties += + """ + true + false + false + """; + } + UpdateProjectFile(projectFilePath, runAnalyzers, extraProperties, extraItems, insertAtEnd); return new ProjectInfo(asset.Name, projectFilePath, logPath, nugetDir); } @@ -186,6 +198,23 @@ public virtual (string projectDir, string buildOutput) BuildProject( return (_projectDir, res.Output); } + if (EnvironmentVariables.UseJavascriptBundler && buildOptions.IsPublish) + { + string publicWwwrootDir = Path.GetFullPath(Path.Combine(GetBinFrameworkDir(configuration, forPublish: true), "..")); + File.Copy(Path.Combine(BuildEnvironment.TestAssetsPath, "JavascriptBundlers", "package.json"), Path.Combine(publicWwwrootDir, "package.json")); + File.Copy(Path.Combine(BuildEnvironment.TestAssetsPath, "JavascriptBundlers", "rollup.config.mjs"), Path.Combine(publicWwwrootDir, "rollup.config.mjs")); + + string npmPath = s_isWindows ? @"C:\Program Files\nodejs\npm.cmd" : "/bin/npm"; + ToolCommand npmCommand = new ToolCommand(npmPath, _testOutput).WithWorkingDirectory(publicWwwrootDir); + npmCommand.Execute("install").EnsureSuccessful(); + npmCommand.Execute("run build").EnsureSuccessful(); + + string publicDir = Path.Combine(publicWwwrootDir, "public"); + File.Copy(Path.Combine(publicWwwrootDir, "index.html"), Path.Combine(publicDir, "index.html")); + + buildOptions = buildOptions with { AssertAppBundle = false }; + } + if (buildOptions.AssertAppBundle) { _provider.AssertWasmSdkBundle(configuration, buildOptions, IsUsingWorkloads, isNativeBuild, wasmFingerprintDotnetJs, res.Output); @@ -270,20 +299,28 @@ public virtual async Task RunForBuildWithDotnetRun(RunOptions runOpti public virtual async Task RunForPublishWithWebServer(RunOptions runOptions) => await BrowserRun(runOptions with { Host = RunHost.WebServer }); - private async Task BrowserRun(RunOptions runOptions) => runOptions.Host switch + private async Task BrowserRun(RunOptions runOptions) { - RunHost.DotnetRun => - await BrowserRunTest($"run -c {runOptions.Configuration} --no-build", _projectDir, runOptions), - - RunHost.WebServer => - await BrowserRunTest($"{s_xharnessRunnerCommand} wasm webserver --app=. --web-server-use-default-files", - string.IsNullOrEmpty(runOptions.CustomBundleDir) ? - Path.GetFullPath(Path.Combine(GetBinFrameworkDir(runOptions.Configuration, forPublish: true), "..")) : - runOptions.CustomBundleDir, - runOptions), - - _ => throw new NotImplementedException(runOptions.Host.ToString()) - }; + if (EnvironmentVariables.UseJavascriptBundler) + { + runOptions = runOptions with { CustomBundleDir = Path.GetFullPath(Path.Combine(GetBinFrameworkDir(runOptions.Configuration, forPublish: true), "..", "public")) }; + } + + return runOptions.Host switch + { + RunHost.DotnetRun => + await BrowserRunTest($"run -c {runOptions.Configuration} --no-build", _projectDir, runOptions), + + RunHost.WebServer => + await BrowserRunTest($"{s_xharnessRunnerCommand} wasm webserver --app=. --web-server-use-default-files", + string.IsNullOrEmpty(runOptions.CustomBundleDir) ? + Path.GetFullPath(Path.Combine(GetBinFrameworkDir(runOptions.Configuration, forPublish: true), "..")) : + runOptions.CustomBundleDir, + runOptions), + + _ => throw new NotImplementedException(runOptions.Host.ToString()) + }; + } private async Task BrowserRunTest(string runArgs, string workingDirectory, diff --git a/src/mono/wasm/Wasm.Build.Tests/Wasm.Build.Tests.csproj b/src/mono/wasm/Wasm.Build.Tests/Wasm.Build.Tests.csproj index a366e7d0782c9b..de2528595ea2e6 100644 --- a/src/mono/wasm/Wasm.Build.Tests/Wasm.Build.Tests.csproj +++ b/src/mono/wasm/Wasm.Build.Tests/Wasm.Build.Tests.csproj @@ -96,6 +96,7 @@ <_XUnitTraitArg Condition="'$(TestUsingWorkloads)' == 'true'">-notrait category=no-workload <_XUnitTraitArg Condition="'$(TestUsingWorkloads)' != 'true'">-trait category=no-workload <_XUnitTraitArg Condition="'$(WasmFingerprintAssets)' == 'false'">-trait category=no-fingerprinting + <_XUnitTraitArg Condition="'$(WasmBundlerFriendlyBootConfig)' == 'true'">-trait category=bundler-friendly @@ -117,6 +118,9 @@ + + + diff --git a/src/mono/wasm/Wasm.Build.Tests/data/RunScriptTemplate.cmd b/src/mono/wasm/Wasm.Build.Tests/data/RunScriptTemplate.cmd index 6cbf9b15d78ccd..608b2c6c1ea37e 100644 --- a/src/mono/wasm/Wasm.Build.Tests/data/RunScriptTemplate.cmd +++ b/src/mono/wasm/Wasm.Build.Tests/data/RunScriptTemplate.cmd @@ -61,6 +61,11 @@ if [%WASM_FINGERPRINT_ASSETS%] == [false] ( ) else ( set USE_FINGERPRINTING_FOR_TESTS=true ) +if [%WASM_BUNDLER_FRIENDLY_BOOT_CONFIG%] == [true] ( + set USE_JAVASCRIPT_BUNDLER_FOR_TESTS=true +) else ( + set USE_JAVASCRIPT_BUNDLER_FOR_TESTS=false +) if [%HELIX_CORRELATION_PAYLOAD%] NEQ [] ( robocopy /mt /np /nfl /NDL /nc /e %BASE_DIR%\%SDK_DIR_NAME% %EXECUTION_DIR%\%SDK_DIR_NAME% diff --git a/src/mono/wasm/Wasm.Build.Tests/data/RunScriptTemplate.sh b/src/mono/wasm/Wasm.Build.Tests/data/RunScriptTemplate.sh index cfbcda4a64307c..cee1769b006639 100644 --- a/src/mono/wasm/Wasm.Build.Tests/data/RunScriptTemplate.sh +++ b/src/mono/wasm/Wasm.Build.Tests/data/RunScriptTemplate.sh @@ -45,6 +45,12 @@ function set_env_vars() export USE_FINGERPRINTING_FOR_TESTS=true fi + if [ "x$WASM_BUNDLER_FRIENDLY_BOOT_CONFIG" = "xtrue" ]; then + export USE_JAVASCRIPT_BUNDLER_FOR_TESTS=true + else + export USE_JAVASCRIPT_BUNDLER_FOR_TESTS=false + fi + local _SDK_DIR= if [[ -n "$HELIX_WORKITEM_UPLOAD_ROOT" ]]; then cp -r $BASE_DIR/$SDK_DIR_NAME $EXECUTION_DIR diff --git a/src/mono/wasm/features.md b/src/mono/wasm/features.md index 50d52a0e03e6db..97f1e8213d988f 100644 --- a/src/mono/wasm/features.md +++ b/src/mono/wasm/features.md @@ -137,11 +137,13 @@ When you want to call JavaScript functions from C# or managed code from JavaScri * or [the documentation](https://learn.microsoft.com/aspnet/core/client-side/dotnet-interop). ### Embedding dotnet in existing JavaScript applications -To embed the .NET runtime inside of a JavaScript application, you will need to use both the MSBuild toolchain (to build and publish your managed code) and your existing web build toolchain. +The default build output relies on exact file names produced during .NET build. In our testing the dynamic loading of assets provides faster startup and shorter download times. -The output of the MSBuild toolchain - located in the [AppBundle](#Project-folder-structure) folder - must be fed in to your web build toolchain in order to ensure that the runtime and managed binaries are deployed with the rest of your application assets. - -For a sample of using the .NET runtime in a React component, [see here](https://github.com/maraf/dotnet-wasm-react). +JavaScript tools like [webpack](https://github.com/webpack/webpack) or [rollup](https://github.com/rollup/rollup) can be used for further file modifications. +An msbuild property `true` can be used to generate different JavaScript files that are not runnable +in the browsers, but they can be consumed by these JavaScript tools. Some examples: + - Merge all JavaScript files, resolve wasm & other files as files, copying them to the output directory, optionally fingerprinting them, etc. + - Embed all JavaScripts files and wasm & other files as base64 encoded blobs directly into a single file. ## Project folder structure @@ -274,11 +276,6 @@ Browsers do not offer a way to access the contents of their time zone database, This requires that you have the [wasm-tools workload](#wasm-tools-workload) installed. -### Bundling JavaScript and other assets -Many web developers use tools like [webpack](https://github.com/webpack/webpack) or [rollup](https://github.com/rollup/rollup) to bundle many files into one large .js file. When deploying a .NET application to the web, you can safely bundle the `dotnet.js` ES6 module with the rest of your JavaScript application, but the other assets and modules in the `_framework` folder may not be bundled as they are loaded dynamically. - -In our testing the dynamic loading of assets provides faster startup and shorter download times. We would like to [hear from the community](https://github.com/dotnet/runtime/issues/86162) if there are scenarios where you need the ability to bundle the rest of an application. - ## Resources consumed on the target device When you deploy a .NET application to the browser, many necessary components and databases are included: - The .NET runtime, including a garbage collector, interpreter, and JIT compiler diff --git a/src/mono/wasm/testassets/JavascriptBundlers/package.json b/src/mono/wasm/testassets/JavascriptBundlers/package.json new file mode 100644 index 00000000000000..13e1957c33262e --- /dev/null +++ b/src/mono/wasm/testassets/JavascriptBundlers/package.json @@ -0,0 +1,26 @@ +{ + "name": "project1", + "version": "1.0.0", + "description": "", + "main": "index.js", + "type": "module", + "scripts": { + "build": "rollup -c" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + }, + "devDependencies": { + "@babel/core": "^7.21.3", + "@rollup/plugin-babel": "^6.0.4", + "@rollup/plugin-commonjs": "^28.0.2", + "@rollup/plugin-image": "^3.0.3", + "@rollup/plugin-node-resolve": "^16.0.0", + "@rollup/plugin-replace": "^6.0.2", + "@rollup/plugin-url": "^8.0.2", + "rollup": "^4.34.6", + "rollup-plugin-import-file": "^1.0.1" + } +} diff --git a/src/mono/wasm/testassets/JavascriptBundlers/rollup.config.mjs b/src/mono/wasm/testassets/JavascriptBundlers/rollup.config.mjs new file mode 100644 index 00000000000000..7ae75e991bb5ca --- /dev/null +++ b/src/mono/wasm/testassets/JavascriptBundlers/rollup.config.mjs @@ -0,0 +1,42 @@ +import nodeResolve from '@rollup/plugin-node-resolve'; +import commonjs from '@rollup/plugin-commonjs'; +import babel from '@rollup/plugin-babel'; +import replace from '@rollup/plugin-replace'; +import files from 'rollup-plugin-import-file'; + +export default { + input: 'main.js', + output: { + file: 'public/main.js', + format: 'esm' + }, + plugins: [ + files({ + output: 'public', + extensions: /\.(wasm|dat)$/, + hash: true, + }), + files({ + output: 'public', + extensions: /\.(json)$/, + }), + nodeResolve({ + extensions: ['.js'] + }), + babel({ + babelHelpers: 'bundled', + extensions: ['.js'], + generatorOpts: { + // Increase the size limit from 500KB to 10MB + compact: true, + retainLines: true, + maxSize: 10000000 + } + }), + commonjs(), + replace({ + preventAssignment: false, + 'process.env.NODE_ENV': '"production"' + }), + ] +} \ No newline at end of file diff --git a/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/BootJsonBuilderHelper.cs b/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/BootJsonBuilderHelper.cs index 5b037474909f0d..f21dc4ed8fc8ac 100644 --- a/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/BootJsonBuilderHelper.cs +++ b/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/BootJsonBuilderHelper.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using System.Text; @@ -18,6 +19,7 @@ public class BootJsonBuilderHelper(TaskLoggingHelper Log, string DebugLevel, boo { #pragma warning disable SYSLIB1045 // Convert to 'GeneratedRegexAttribute'. internal static readonly Regex mergeWithPlaceholderRegex = new Regex(@"/\*!\s*dotnetBootConfig\s*\*/\s*{}"); + internal static readonly Regex bundlerFriendlyImportsRegex = new Regex(@"/\*!\s*bundlerFriendlyImports\s*\*/"); #pragma warning restore SYSLIB1045 // Convert to 'GeneratedRegexAttribute'. private static readonly string[] coreAssemblyNames = [ @@ -52,18 +54,33 @@ public bool IsCoreAssembly(string fileName) return false; } - public void WriteConfigToFile(BootJsonData config, string outputPath, string? outputFileExtension = null, string? mergeWith = null) + public void WriteConfigToFile(BootJsonData config, string outputPath, string? outputFileExtension = null, string? mergeWith = null, string? imports = null) { var output = JsonSerializer.Serialize(config, JsonOptions); + // Remove the $#[ and ]#$" that are used to mark JS variable usage. + output = output + .Replace("\"$#[", string.Empty) + .Replace("]#$\"", string.Empty); + outputFileExtension ??= Path.GetExtension(outputPath); Log.LogMessage($"Write config in format '{outputFileExtension}'"); if (mergeWith != null) { string existingContent = File.ReadAllText(mergeWith); - output = mergeWithPlaceholderRegex.Replace(existingContent, e => $"/*json-start*/{output}/*json-end*/"); - if (existingContent.Equals(output)) - Log.LogError($"Merging boot config into '{mergeWith}' failed to find the placeholder."); + output = ReplaceWithAssert( + mergeWithPlaceholderRegex, + existingContent, + $"/*json-start*/{output}/*json-end*/", + $"Merging boot config into '{mergeWith}' failed to find the placeholder." + ); + + output = ReplaceWithAssert( + bundlerFriendlyImportsRegex, + output, + imports ?? string.Empty, + $"Failed to find the placeholder for bundler friendly imports." + ); } else if (outputFileExtension == ".js") { @@ -73,8 +90,20 @@ public void WriteConfigToFile(BootJsonData config, string outputPath, string? ou File.WriteAllText(outputPath, output); } + private string ReplaceWithAssert(Regex regex, string content, string replacement, string errorMessage) + { + string existingContent = content; + content = regex.Replace(content, e => replacement); + if (existingContent.Equals(content)) + Log.LogError(errorMessage); + + return content; + } + public void ComputeResourcesHash(BootJsonData bootConfig) { + ResourcesData resources = (ResourcesData)bootConfig.resources; + var sb = new StringBuilder(); static void AddDictionary(StringBuilder sb, Dictionary? res) @@ -86,59 +115,61 @@ static void AddDictionary(StringBuilder sb, Dictionary? res) sb.Append(assetHash); } - AddDictionary(sb, bootConfig.resources.assembly); - AddDictionary(sb, bootConfig.resources.coreAssembly); + AddDictionary(sb, resources.assembly); + AddDictionary(sb, resources.coreAssembly); - AddDictionary(sb, bootConfig.resources.jsModuleWorker); - AddDictionary(sb, bootConfig.resources.jsModuleDiagnostics); - AddDictionary(sb, bootConfig.resources.jsModuleNative); - AddDictionary(sb, bootConfig.resources.jsModuleRuntime); - AddDictionary(sb, bootConfig.resources.wasmNative); - AddDictionary(sb, bootConfig.resources.wasmSymbols); - AddDictionary(sb, bootConfig.resources.icu); - AddDictionary(sb, bootConfig.resources.runtime); - AddDictionary(sb, bootConfig.resources.lazyAssembly); + AddDictionary(sb, resources.jsModuleWorker); + AddDictionary(sb, resources.jsModuleDiagnostics); + AddDictionary(sb, resources.jsModuleNative); + AddDictionary(sb, resources.jsModuleRuntime); + AddDictionary(sb, resources.wasmNative); + AddDictionary(sb, resources.wasmSymbols); + AddDictionary(sb, resources.icu); + AddDictionary(sb, resources.runtime); + AddDictionary(sb, resources.lazyAssembly); - if (bootConfig.resources.satelliteResources != null) + if (resources.satelliteResources != null) { - foreach (var culture in bootConfig.resources.satelliteResources) + foreach (var culture in resources.satelliteResources) AddDictionary(sb, culture.Value); } - if (bootConfig.resources.vfs != null) + if (resources.vfs != null) { - foreach (var entry in bootConfig.resources.vfs) + foreach (var entry in resources.vfs) AddDictionary(sb, entry.Value); } - if (bootConfig.resources.coreVfs != null) + if (resources.coreVfs != null) { - foreach (var entry in bootConfig.resources.coreVfs) + foreach (var entry in resources.coreVfs) AddDictionary(sb, entry.Value); } - bootConfig.resources.hash = Utils.ComputeTextIntegrity(sb.ToString()); + resources.hash = Utils.ComputeTextIntegrity(sb.ToString()); } public Dictionary? GetNativeResourceTargetInBootConfig(BootJsonData bootConfig, string resourceName) { + ResourcesData resources = (ResourcesData)bootConfig.resources; + string resourceExtension = Path.GetExtension(resourceName); if (resourceName.StartsWith("dotnet.native.worker", StringComparison.OrdinalIgnoreCase) && string.Equals(resourceExtension, ".mjs", StringComparison.OrdinalIgnoreCase)) - return bootConfig.resources.jsModuleWorker ??= new(); + return resources.jsModuleWorker ??= new(); else if (resourceName.StartsWith("dotnet.diagnostics", StringComparison.OrdinalIgnoreCase) && string.Equals(resourceExtension, ".js", StringComparison.OrdinalIgnoreCase)) - return bootConfig.resources.jsModuleDiagnostics ??= new(); + return resources.jsModuleDiagnostics ??= new(); else if (resourceName.StartsWith("dotnet.native", StringComparison.OrdinalIgnoreCase) && string.Equals(resourceExtension, ".js", StringComparison.OrdinalIgnoreCase)) - return bootConfig.resources.jsModuleNative ??= new(); + return resources.jsModuleNative ??= new(); else if (resourceName.StartsWith("dotnet.runtime", StringComparison.OrdinalIgnoreCase) && string.Equals(resourceExtension, ".js", StringComparison.OrdinalIgnoreCase)) - return bootConfig.resources.jsModuleRuntime ??= new(); + return resources.jsModuleRuntime ??= new(); else if (resourceName.StartsWith("dotnet.native", StringComparison.OrdinalIgnoreCase) && string.Equals(resourceExtension, ".wasm", StringComparison.OrdinalIgnoreCase)) - return bootConfig.resources.wasmNative ??= new(); + return 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.wasmSymbols ??= new(); + return resources.wasmSymbols ??= new(); else if (resourceName.StartsWith("icudt", StringComparison.OrdinalIgnoreCase)) - return bootConfig.resources.icu ??= new(); + return resources.icu ??= new(); else Log.LogError($"The resource '{resourceName}' is not recognized as any native asset"); @@ -174,5 +205,139 @@ public int GetDebugLevel(bool hasPdb) return intValue; } + + public string TransformResourcesToAssets(BootJsonData config, bool bundlerFriendly = false) + { + List imports = []; + + ResourcesData resources = (ResourcesData)config.resources; + var assets = new AssetsData(); + + assets.hash = resources.hash; + assets.jsModuleRuntime = MapJsAssets(resources.jsModuleRuntime); + assets.jsModuleNative = MapJsAssets(resources.jsModuleNative); + assets.jsModuleWorker = MapJsAssets(resources.jsModuleWorker); + assets.jsModuleDiagnostics = MapJsAssets(resources.jsModuleDiagnostics); + + assets.wasmNative = resources.wasmNative?.Select(a => + { + var asset = new WasmAsset() + { + name = a.Key, + integrity = a.Value + }; + + if (bundlerFriendly) + { + string escaped = EscapeName(a.Key); + imports.Add($"import {escaped} from \"./{a.Key}\";"); + asset.resolvedUrl = EncodeJavascriptVariableInJson(escaped); + } + + return asset; + }).ToList(); + assets.wasmSymbols = resources.wasmSymbols?.Select(a => new SymbolsAsset() + { + name = a.Key, + }).ToList(); + + assets.icu = MapGeneralAssets(resources.icu); + assets.coreAssembly = MapGeneralAssets(resources.coreAssembly); + assets.assembly = MapGeneralAssets(resources.assembly); + assets.corePdb = MapGeneralAssets(resources.corePdb); + assets.pdb = MapGeneralAssets(resources.pdb); + assets.lazyAssembly = MapGeneralAssets(resources.lazyAssembly); + + if (resources.satelliteResources != null) + { + assets.satelliteResources = resources.satelliteResources.ToDictionary( + kvp => kvp.Key, + kvp => MapGeneralAssets(kvp.Value, variableNamePrefix: kvp.Key, subFolder: kvp.Key) + ); + } + + assets.libraryInitializers = resources.libraryInitializers; + assets.modulesAfterConfigLoaded = MapJsAssets(resources.modulesAfterConfigLoaded); + assets.modulesAfterRuntimeReady = MapJsAssets(resources.modulesAfterRuntimeReady); + + assets.extensions = resources.extensions; + + assets.coreVfs = MapVfsAssets(resources.coreVfs); + assets.vfs = MapVfsAssets(resources.vfs); + + if (bundlerFriendly && config.appsettings != null) + { + config.appsettings = config.appsettings.Select(a => + { + string escaped = EscapeName(a); + imports.Add($"import {escaped} from \"./{a}\";"); + return EncodeJavascriptVariableInJson(escaped); + }).ToList(); + } + + string EscapeName(string name) => Utils.FixupSymbolName(name); + string EncodeJavascriptVariableInJson(string name) => $"$#[{name}]#$"; + + List? MapGeneralAssets(Dictionary? assets, string? variableNamePrefix = null, string? subFolder = null) => assets?.Select(a => + { + var asset = new GeneralAsset() + { + virtualPath = resources.fingerprinting?[a.Key] ?? a.Key, + name = a.Key, + integrity = a.Value + }; + + if (bundlerFriendly) + { + string escaped = EscapeName(string.Concat(subFolder, a.Key)); + string subFolderWithSeparator = subFolder != null ? $"{subFolder}/" : string.Empty; + imports.Add($"import {escaped} from \"./{subFolderWithSeparator}{a.Key}\";"); + asset.resolvedUrl = EncodeJavascriptVariableInJson(escaped); + } + + return asset; + }).ToList(); + + List? MapJsAssets(Dictionary? assets, string? variableNamePrefix = null, string? subFolder = null) => assets?.Select(a => + { + var asset = new JsAsset() + { + name = a.Key + }; + + if (bundlerFriendly) + { + string escaped = EscapeName(string.Concat(subFolder, a.Key)); + string subFolderWithSeparator = subFolder != null ? $"{subFolder}/" : string.Empty; + imports.Add($"import * as {escaped} from \"./{subFolderWithSeparator}{a.Key}\";"); + asset.moduleExports = EncodeJavascriptVariableInJson(escaped); + } + + return asset; + }).ToList(); + + List? MapVfsAssets(Dictionary>? assets) => assets?.Select(a => + { + var asset = new VfsAsset() + { + virtualPath = a.Key, + name = a.Value.Keys.First(), + integrity = a.Value.Values.First() + }; + + if (bundlerFriendly) + { + string escaped = EscapeName(string.Concat(asset.name)); + imports.Add($"import * as {escaped} from \"./{asset.name}\";"); + asset.resolvedUrl = EncodeJavascriptVariableInJson(escaped); + } + + return asset; + }).ToList(); + + config.resources = assets; + + return string.Join(Environment.NewLine, imports); + } } } 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 3a2eee6dca5abe..92d618f88d84f1 100644 --- a/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/BootJsonData.cs +++ b/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/BootJsonData.cs @@ -28,6 +28,9 @@ public class BootJsonData public string applicationEnvironment { get; set; } /// + /// For .NET < 10, this contains . + /// For .NET >= 10, this contains . + /// --- /// 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, /// and any PDBs to be loaded. @@ -36,7 +39,8 @@ public class BootJsonData /// and values are SHA-256 hashes formatted in prefixed base-64 style (e.g., 'sha256-abcdefg...') /// as used for subresource integrity checking. /// - public ResourcesData resources { get; set; } = new ResourcesData(); + [DataMember(EmitDefaultValue = false)] + public object resources { get; set; } = new ResourcesData(); /// /// Gets a value that determines whether to enable caching of the @@ -249,6 +253,133 @@ public class ResourcesData public List remoteSources { get; set; } } +public class AssetsData +{ + /// + /// Gets a hash of all resources + /// + public string hash { get; set; } + + [DataMember(EmitDefaultValue = false)] + public List jsModuleWorker { get; set; } + + [DataMember(EmitDefaultValue = false)] + public List jsModuleDiagnostics { get; set; } + + [DataMember(EmitDefaultValue = false)] + public List jsModuleNative { get; set; } + + [DataMember(EmitDefaultValue = false)] + public List jsModuleRuntime { get; set; } + + [DataMember(EmitDefaultValue = false)] + public List wasmNative { get; set; } + + [DataMember(EmitDefaultValue = false)] + public List wasmSymbols { get; set; } + + [DataMember(EmitDefaultValue = false)] + public List icu { get; set; } + + /// + /// "assembly" (.dll) resources needed to start MonoVM + /// + public List coreAssembly { get; set; } = new(); + + /// + /// "assembly" (.dll) resources + /// + public List assembly { get; set; } = new(); + + /// + /// "debug" (.pdb) resources needed to start MonoVM + /// + [DataMember(EmitDefaultValue = false)] + public List corePdb { get; set; } + + /// + /// "debug" (.pdb) resources + /// + [DataMember(EmitDefaultValue = false)] + public List pdb { get; set; } + + /// + /// localization (.satellite resx) resources + /// + [DataMember(EmitDefaultValue = false)] + public Dictionary> satelliteResources { get; set; } + + /// + /// Assembly (.dll) resources that are loaded lazily during runtime + /// + [DataMember(EmitDefaultValue = false)] + public List lazyAssembly { get; set; } + + /// + /// JavaScript module initializers that Blazor will be in charge of loading. + /// + [DataMember(EmitDefaultValue = false)] + public ResourceHashesByNameDictionary libraryInitializers { get; set; } + + [DataMember(EmitDefaultValue = false)] + public List modulesAfterConfigLoaded { get; set; } + + [DataMember(EmitDefaultValue = false)] + public List modulesAfterRuntimeReady { get; set; } + + /// + /// Extensions created by users customizing the initialization process. The format of the file(s) + /// is up to the user. + /// + [DataMember(EmitDefaultValue = false)] + public Dictionary extensions { get; set; } + + [DataMember(EmitDefaultValue = false)] + public List coreVfs { get; set; } + + [DataMember(EmitDefaultValue = false)] + public List vfs { get; set; } +} + +[DataContract] +public class JsAsset +{ + public string name { get; set; } + public string moduleExports { get; set; } +} + +[DataContract] +public class SymbolsAsset +{ + public string name { get; set; } +} + +[DataContract] +public class WasmAsset +{ + public string name { get; set; } + public string integrity { get; set; } + public string resolvedUrl { get; set; } +} + +[DataContract] +public class GeneralAsset +{ + public string virtualPath { get; set; } + public string name { get; set; } + public string integrity { get; set; } + public string resolvedUrl { get; set; } +} + +[DataContract] +public class VfsAsset +{ + public string virtualPath { get; set; } + public string name { get; set; } + public string integrity { get; set; } + public string resolvedUrl { get; set; } +} + public enum GlobalizationMode : int { // Note that the numeric values are serialized and used in JS code, so don't change them without also updating the JS code 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 711c3d97c57f5f..0e888b4bb7eec5 100644 --- a/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/GenerateWasmBootJson.cs +++ b/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/GenerateWasmBootJson.cs @@ -92,6 +92,8 @@ public class GenerateWasmBootJson : Task public string MergeWith { get; set; } + public bool BundlerFriendly { get; set; } + public override bool Execute() { var entryAssemblyName = AssemblyName.GetAssemblyName(AssemblyPath).Name; @@ -183,6 +185,7 @@ private void WriteBootConfig(string entryAssemblyName) // - runtime: // - UriPath (e.g., "dotnet.js") // - ContentHash (e.g., "3448f339acf512448") + ResourcesData resourceData = (ResourcesData)result.resources; if (Resources != null) { var endpointByAsset = Endpoints.ToDictionary(e => e.GetMetadata("AssetFile")); @@ -197,7 +200,6 @@ private void WriteBootConfig(string entryAssemblyName) }); var remainingLazyLoadAssemblies = new List(LazyLoadedAssemblies ?? Array.Empty()); - var resourceData = result.resources; if (FingerprintAssets) resourceData.fingerprinting = new(); @@ -394,7 +396,7 @@ private void WriteBootConfig(string entryAssemblyName) if (IsTargeting80OrLater()) { - result.debugLevel = helper.GetDebugLevel(result.resources?.pdb?.Count > 0); + result.debugLevel = helper.GetDebugLevel(resourceData.pdb?.Count > 0); } if (ConfigurationFiles != null) @@ -454,7 +456,12 @@ private void WriteBootConfig(string entryAssemblyName) } helper.ComputeResourcesHash(result); - helper.WriteConfigToFile(result, OutputPath, mergeWith: MergeWith); + + string? imports = null; + if (IsTargeting100OrLater()) + imports = helper.TransformResourcesToAssets(result, BundlerFriendly); + + helper.WriteConfigToFile(result, OutputPath, mergeWith: MergeWith, imports: imports); void AddResourceToList(ITaskItem resource, ResourceHashesByNameDictionary resourceList, string resourceKey) { diff --git a/src/tasks/WasmAppBuilder/WasmAppBuilder.cs b/src/tasks/WasmAppBuilder/WasmAppBuilder.cs index 3a67adf910ab1c..ae2aa23c93e796 100644 --- a/src/tasks/WasmAppBuilder/WasmAppBuilder.cs +++ b/src/tasks/WasmAppBuilder/WasmAppBuilder.cs @@ -194,6 +194,8 @@ protected override bool ExecuteInternal() File.WriteAllText(packageJsonPath, json); } + ResourcesData resources = (ResourcesData)bootConfig.resources; + foreach (var assembly in _assemblies) { string assemblyPath = assembly; @@ -215,7 +217,7 @@ protected override bool ExecuteInternal() var assemblyName = Path.GetFileName(assemblyPath); bool isCoreAssembly = IsAot || helper.IsCoreAssembly(assemblyName); - var assemblyList = isCoreAssembly ? bootConfig.resources.coreAssembly : bootConfig.resources.assembly; + var assemblyList = isCoreAssembly ? resources.coreAssembly : resources.assembly; assemblyList[assemblyName] = Utils.ComputeIntegrity(bytes); if (baseDebugLevel != 0) @@ -224,29 +226,22 @@ protected override bool ExecuteInternal() if (File.Exists(pdb)) { if (isCoreAssembly) - { - if (bootConfig.resources.corePdb == null) - bootConfig.resources.corePdb = new(); - } + resources.corePdb ??= new(); else - { - if (bootConfig.resources.pdb == null) - bootConfig.resources.pdb = new(); - } + resources.pdb ??= new(); - var pdbList = isCoreAssembly ? bootConfig.resources.corePdb : bootConfig.resources.pdb; + var pdbList = isCoreAssembly ? resources.corePdb : resources.pdb; pdbList[Path.GetFileName(pdb)] = Utils.ComputeIntegrity(pdb); } } } } - bootConfig.debugLevel = helper.GetDebugLevel(bootConfig.resources.pdb?.Count > 0); + bootConfig.debugLevel = helper.GetDebugLevel(resources.pdb?.Count > 0); ProcessSatelliteAssemblies(args => { - if (bootConfig.resources.satelliteResources == null) - bootConfig.resources.satelliteResources = new(); + resources.satelliteResources ??= new(); string name = Path.GetFileName(args.fullPath); string cultureDirectory = Path.Combine(runtimeAssetsPath, args.culture); @@ -263,8 +258,8 @@ protected override bool ExecuteInternal() Log.LogMessage(MessageImportance.Low, $"Skipped generating {finalWebcil} as the contents are unchanged."); _fileWrites.Add(finalWebcil); - if (!bootConfig.resources.satelliteResources.TryGetValue(args.culture, out var cultureSatelliteResources)) - bootConfig.resources.satelliteResources[args.culture] = cultureSatelliteResources = new(); + if (!resources.satelliteResources.TryGetValue(args.culture, out var cultureSatelliteResources)) + resources.satelliteResources[args.culture] = cultureSatelliteResources = new(); cultureSatelliteResources[Path.GetFileName(finalWebcil)] = Utils.ComputeIntegrity(finalWebcil); } @@ -273,8 +268,8 @@ protected override bool ExecuteInternal() var satellitePath = Path.Combine(cultureDirectory, name); FileCopyChecked(args.fullPath, satellitePath, "SatelliteAssemblies"); - if (!bootConfig.resources.satelliteResources.TryGetValue(args.culture, out var cultureSatelliteResources)) - bootConfig.resources.satelliteResources[args.culture] = cultureSatelliteResources = new(); + if (!resources.satelliteResources.TryGetValue(args.culture, out var cultureSatelliteResources)) + resources.satelliteResources[args.culture] = cultureSatelliteResources = new(); cultureSatelliteResources[name] = Utils.ComputeIntegrity(satellitePath); } @@ -334,10 +329,10 @@ protected override bool ExecuteInternal() } if (vfs.Count > 0) - bootConfig.resources.vfs = vfs; + resources.vfs = vfs; if (coreVfs.Count > 0) - bootConfig.resources.coreVfs = coreVfs; + resources.coreVfs = coreVfs; } if (!InvariantGlobalization) @@ -351,18 +346,18 @@ protected override bool ExecuteInternal() return false; } - bootConfig.resources.icu ??= new(); - bootConfig.resources.icu[Path.GetFileName(idfn)] = Utils.ComputeIntegrity(idfn); + resources.icu ??= new(); + resources.icu[Path.GetFileName(idfn)] = Utils.ComputeIntegrity(idfn); } } if (RemoteSources?.Length > 0) { - bootConfig.resources.remoteSources = new(); + resources.remoteSources = new(); foreach (var source in RemoteSources) if (source != null && source.ItemSpec != null) - bootConfig.resources.remoteSources.Add(source.ItemSpec); + resources.remoteSources.Add(source.ItemSpec); } var extraConfiguration = new Dictionary(); @@ -446,6 +441,7 @@ protected override bool ExecuteInternal() using TempFileName tmpConfigPath = new(); { helper.ComputeResourcesHash(bootConfig); + helper.TransformResourcesToAssets(bootConfig); helper.WriteConfigToFile(bootConfig, tmpConfigPath.Path, Path.GetExtension(ConfigFileName)); }