Add support for loading satellite assemblies (#20033)
* Add support for loading satellite assemblies Fixes #17016
This commit is contained in:
parent
cb6858fe31
commit
82d05ae785
File diff suppressed because one or more lines are too long
|
|
@ -32,7 +32,7 @@ export interface ResourceGroups {
|
|||
readonly assembly: ResourceList;
|
||||
readonly pdb?: ResourceList;
|
||||
readonly runtime: ResourceList;
|
||||
readonly satelliteResources?: { [cultureName: string] : ResourceList };
|
||||
}
|
||||
|
||||
export type ResourceList = { [name: string]: string };
|
||||
|
||||
|
|
|
|||
|
|
@ -207,6 +207,37 @@ function createEmscriptenModuleInstance(resourceLoader: WebAssemblyResourceLoade
|
|||
// of the extensions in the URLs. This allows loading assemblies with arbitrary filenames.
|
||||
assembliesBeingLoaded.forEach(r => addResourceAsAssembly(r, changeExtension(r.name, '.dll')));
|
||||
pdbsBeingLoaded.forEach(r => addResourceAsAssembly(r, r.name));
|
||||
|
||||
// Wire-up callbacks for satellite assemblies. Blazor will call these as part of the application
|
||||
// startup sequence to load satellite assemblies for the application's culture.
|
||||
window['Blazor']._internal.getSatelliteAssemblies = (culturesToLoadDotNetArray: System_Array<System_String>) : System_Object => {
|
||||
const culturesToLoad = BINDING.mono_array_to_js_array<System_String, string>(culturesToLoadDotNetArray);
|
||||
const satelliteResources = resourceLoader.bootConfig.resources.satelliteResources;
|
||||
|
||||
if (satelliteResources) {
|
||||
const resourcePromises = Promise.all(culturesToLoad
|
||||
.filter(culture => satelliteResources.hasOwnProperty(culture))
|
||||
.map(culture => resourceLoader.loadResources(satelliteResources[culture], fileName => `_framework/_bin/${fileName}`))
|
||||
.reduce((previous, next) => previous.concat(next), new Array<LoadingResource>())
|
||||
.map(async resource => (await resource.response).arrayBuffer()));
|
||||
|
||||
return BINDING.js_to_mono_obj(
|
||||
resourcePromises.then(resourcesToLoad => {
|
||||
if (resourcesToLoad.length) {
|
||||
window['Blazor']._internal.readSatelliteAssemblies = () => {
|
||||
const array = BINDING.mono_obj_array_new(resourcesToLoad.length);
|
||||
for (var i = 0; i < resourcesToLoad.length; i++) {
|
||||
BINDING.mono_obj_array_set(array, i, BINDING.js_typed_array_to_array(new Uint8Array(resourcesToLoad[i])));
|
||||
}
|
||||
return array;
|
||||
};
|
||||
}
|
||||
|
||||
return resourcesToLoad.length;
|
||||
}));
|
||||
}
|
||||
return BINDING.js_to_mono_obj(Promise.resolve(0));
|
||||
}
|
||||
});
|
||||
|
||||
module.postRun.push(() => {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Pointer, System_String } from '../Platform';
|
||||
import { Pointer, System_String, System_Array, System_Object } from '../Platform';
|
||||
|
||||
// Mono uses this global to hang various debugging-related items on
|
||||
|
||||
|
|
@ -10,9 +10,12 @@ declare interface MONO {
|
|||
|
||||
// Mono uses this global to hold low-level interop APIs
|
||||
declare interface BINDING {
|
||||
mono_obj_array_new(length: number): System_Array<System_Object>;
|
||||
mono_obj_array_set(array: System_Array<System_Object>, index: Number, value: System_Object): void;
|
||||
js_string_to_mono_string(jsString: string): System_String;
|
||||
js_typed_array_to_array(array: Uint8Array): Pointer;
|
||||
js_typed_array_to_array<T>(array: Array<T>): Pointer;
|
||||
js_typed_array_to_array(array: Uint8Array): System_Object;
|
||||
js_to_mono_obj(jsObject: any) : System_Object;
|
||||
mono_array_to_js_array<TInput, TOutput>(array: System_Array<TInput>) : Array<TOutput>;
|
||||
conv_string(dotnetString: System_String | null): string | null;
|
||||
bind_static_method(fqn: string, signature?: string): Function;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { BootConfigResult } from './BootConfig';
|
||||
import { System_String, Pointer } from './Platform';
|
||||
import { System_String, System_Object } from './Platform';
|
||||
|
||||
export class WebAssemblyConfigLoader {
|
||||
static async initAsync(bootConfigResult: BootConfigResult): Promise<void> {
|
||||
|
|
@ -9,7 +9,7 @@ export class WebAssemblyConfigLoader {
|
|||
.filter(name => name === 'appsettings.json' || name === `appsettings.${bootConfigResult.applicationEnvironment}.json`)
|
||||
.map(async name => ({ name, content: await getConfigBytes(name) })));
|
||||
|
||||
window['Blazor']._internal.getConfig = (dotNetFileName: System_String) : Pointer | undefined => {
|
||||
window['Blazor']._internal.getConfig = (dotNetFileName: System_String) : System_Object | undefined => {
|
||||
const fileName = BINDING.conv_string(dotNetFileName);
|
||||
const resolvedFile = configFiles.find(f => f.name === fileName);
|
||||
return resolvedFile ? BINDING.js_typed_array_to_array(resolvedFile.content) : undefined;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Runtime.Serialization.Json;
|
||||
using System.Text;
|
||||
using Microsoft.Build.Framework;
|
||||
|
|
@ -61,28 +62,56 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Build
|
|||
cacheBootResources = CacheBootResources,
|
||||
debugBuild = DebugBuild,
|
||||
linkerEnabled = LinkerEnabled,
|
||||
resources = new Dictionary<ResourceType, ResourceHashesByNameDictionary>(),
|
||||
resources = new ResourcesData(),
|
||||
config = new List<string>(),
|
||||
};
|
||||
|
||||
// Build a two-level dictionary of the form:
|
||||
// - BootResourceType (e.g., "assembly")
|
||||
// - assembly:
|
||||
// - UriPath (e.g., "System.Text.Json.dll")
|
||||
// - ContentHash (e.g., "4548fa2e9cf52986")
|
||||
// - runtime:
|
||||
// - UriPath (e.g., "dotnet.js")
|
||||
// - ContentHash (e.g., "3448f339acf512448")
|
||||
if (Resources != null)
|
||||
{
|
||||
var resourceData = result.resources;
|
||||
foreach (var resource in Resources)
|
||||
{
|
||||
var resourceTypeMetadata = resource.GetMetadata("BootManifestResourceType");
|
||||
if (!Enum.TryParse<ResourceType>(resourceTypeMetadata, out var resourceType))
|
||||
ResourceHashesByNameDictionary resourceList;
|
||||
switch (resourceTypeMetadata)
|
||||
{
|
||||
throw new NotSupportedException($"Unsupported BootManifestResourceType metadata value: {resourceTypeMetadata}");
|
||||
}
|
||||
case "runtime":
|
||||
resourceList = resourceData.runtime;
|
||||
break;
|
||||
case "assembly":
|
||||
resourceList = resourceData.assembly;
|
||||
break;
|
||||
case "pdb":
|
||||
resourceData.pdb = new ResourceHashesByNameDictionary();
|
||||
resourceList = resourceData.pdb;
|
||||
break;
|
||||
case "satellite":
|
||||
if (resourceData.satelliteResources is null)
|
||||
{
|
||||
resourceData.satelliteResources = new Dictionary<string, ResourceHashesByNameDictionary>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
var resourceCulture = resource.GetMetadata("Culture");
|
||||
if (resourceCulture is null)
|
||||
{
|
||||
Log.LogWarning("Satellite resource {0} does not specify required metadata 'Culture'.", resource);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!result.resources.TryGetValue(resourceType, out var resourceList))
|
||||
{
|
||||
resourceList = new ResourceHashesByNameDictionary();
|
||||
result.resources.Add(resourceType, resourceList);
|
||||
if (!resourceData.satelliteResources.TryGetValue(resourceCulture, out resourceList))
|
||||
{
|
||||
resourceList = new ResourceHashesByNameDictionary();
|
||||
resourceData.satelliteResources.Add(resourceCulture, resourceList);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new NotSupportedException($"Unsupported BootManifestResourceType metadata value: {resourceTypeMetadata}");
|
||||
}
|
||||
|
||||
var resourceName = GetResourceName(resource);
|
||||
|
|
@ -142,7 +171,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Build
|
|||
/// and values are SHA-256 hashes formatted in prefixed base-64 style (e.g., 'sha256-abcdefg...')
|
||||
/// as used for subresource integrity checking.
|
||||
/// </summary>
|
||||
public Dictionary<ResourceType, ResourceHashesByNameDictionary> resources { get; set; }
|
||||
public ResourcesData resources { get; set; } = new ResourcesData();
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value that determines whether to enable caching of the <see cref="resources"/>
|
||||
|
|
@ -166,11 +195,29 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Build
|
|||
public List<string> config { get; set; }
|
||||
}
|
||||
|
||||
public enum ResourceType
|
||||
public class ResourcesData
|
||||
{
|
||||
assembly,
|
||||
pdb,
|
||||
runtime,
|
||||
/// <summary>
|
||||
/// .NET Wasm runtime resources (dotnet.wasm, dotnet.js) etc.
|
||||
/// </summary>
|
||||
public ResourceHashesByNameDictionary runtime { get; set; } = new ResourceHashesByNameDictionary();
|
||||
|
||||
/// <summary>
|
||||
/// "assembly" (.dll) resources
|
||||
/// </summary>
|
||||
public ResourceHashesByNameDictionary assembly { get; set; } = new ResourceHashesByNameDictionary();
|
||||
|
||||
/// <summary>
|
||||
/// "debug" (.pdb) resources
|
||||
/// </summary>
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public ResourceHashesByNameDictionary pdb { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// localization (.satellite resx) resources
|
||||
/// </summary>
|
||||
[DataMember(EmitDefaultValue = false)]
|
||||
public Dictionary<string, ResourceHashesByNameDictionary> satelliteResources { get; set; }
|
||||
}
|
||||
#pragma warning restore IDE1006 // Naming Styles
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,8 +5,7 @@
|
|||
<AdditionalMonoLinkerOptions>--disable-opt unreachablebodies --verbose --strip-security true --exclude-feature com -v false -c link -u link -b true</AdditionalMonoLinkerOptions>
|
||||
|
||||
<_BlazorJsPath Condition="'$(_BlazorJsPath)' == ''">$(MSBuildThisFileDirectory)..\tools\blazor\blazor.webassembly.js</_BlazorJsPath>
|
||||
<_BaseBlazorDistPath>dist\</_BaseBlazorDistPath>
|
||||
<_BaseBlazorRuntimeOutputPath>$(_BaseBlazorDistPath)_framework\</_BaseBlazorRuntimeOutputPath>
|
||||
<_BaseBlazorRuntimeOutputPath>_framework\</_BaseBlazorRuntimeOutputPath>
|
||||
<_BlazorRuntimeBinOutputPath>$(_BaseBlazorRuntimeOutputPath)_bin\</_BlazorRuntimeBinOutputPath>
|
||||
<_BlazorRuntimeWasmOutputPath>$(_BaseBlazorRuntimeOutputPath)wasm\</_BlazorRuntimeWasmOutputPath>
|
||||
<_BlazorBuiltInBclLinkerDescriptor>$(MSBuildThisFileDirectory)BuiltInBclLinkerDescriptor.xml</_BlazorBuiltInBclLinkerDescriptor>
|
||||
|
|
|
|||
|
|
@ -50,14 +50,14 @@
|
|||
-->
|
||||
<ItemGroup>
|
||||
<!-- Assemblies from packages -->
|
||||
<_BlazorManagedRuntimeAssemby Include="@(RuntimeCopyLocalItems)" />
|
||||
<_BlazorManagedRuntimeAssembly Include="@(RuntimeCopyLocalItems)" />
|
||||
|
||||
<!-- Assemblies from other references -->
|
||||
<_BlazorUserRuntimeAssembly Include="@(ReferencePath->WithMetadataValue('CopyLocal', 'true'))" />
|
||||
<_BlazorUserRuntimeAssembly Include="@(ReferenceDependencyPaths->WithMetadataValue('CopyLocal', 'true'))" />
|
||||
|
||||
<_BlazorManagedRuntimeAssemby Include="@(_BlazorUserRuntimeAssembly)" />
|
||||
<_BlazorManagedRuntimeAssemby Include="@(IntermediateAssembly)" />
|
||||
<_BlazorManagedRuntimeAssembly Include="@(_BlazorUserRuntimeAssembly)" />
|
||||
<_BlazorManagedRuntimeAssembly Include="@(IntermediateAssembly)" />
|
||||
</ItemGroup>
|
||||
|
||||
<MakeDir Directories="$(_BlazorIntermediateOutputPath)" />
|
||||
|
|
@ -70,26 +70,57 @@
|
|||
satellite assemblies, this should include all assemblies needed to run the application.
|
||||
-->
|
||||
<ItemGroup>
|
||||
<_BlazorJSFile Include="$(_BlazorJSPath)" />
|
||||
<_BlazorJSFile Include="$(_BlazorJSMapPath)" Condition="Exists('$(_BlazorJSMapPath)')" />
|
||||
<_DotNetWasmRuntimeFile Include="$(ComponentsWebAssemblyRuntimePath)*" />
|
||||
|
||||
<!--
|
||||
ReferenceCopyLocalPaths includes all files that are part of the build out with CopyLocalLockFileAssemblies on.
|
||||
Remove assemblies that are inputs to calculating the assembly closure. Instead use the resolved outputs, since it is the minimal set.
|
||||
|
||||
ReferenceCopyLocalPaths also includes satellite assemblies from referenced projects but are inexpicably missing
|
||||
any metadata that might allow them to be differentiated. We'll explicitly add those
|
||||
to _BlazorOutputWithTargetPath so that satellite assemblies from packages, the current project and referenced project
|
||||
are all treated the same.
|
||||
-->
|
||||
<_BlazorCopyLocalPaths Include="@(ReferenceCopyLocalPaths)" Condition="'%(Extension)' == '.dll'" />
|
||||
<_BlazorCopyLocalPaths Remove="@(_BlazorManagedRuntimeAssemby)" />
|
||||
<_BlazorCopyLocalPaths Include="@(ReferenceCopyLocalPaths)"
|
||||
Exclude="@(_BlazorManagedRuntimeAssembly);@(ReferenceSatellitePaths)"
|
||||
Condition="'%(Extension)' == '.dll'" />
|
||||
|
||||
<_BlazorCopyLocalPaths Include="@(IntermediateSatelliteAssembliesWithTargetPath)">
|
||||
<DestinationSubDirectory>%(IntermediateSatelliteAssembliesWithTargetPath.Culture)\</DestinationSubDirectory>
|
||||
</_BlazorCopyLocalPaths>
|
||||
|
||||
<_BlazorOutputWithTargetPath Include="@(_BlazorCopyLocalPaths)">
|
||||
<!-- This group is for satellite assemblies. We set the resource name to include a path, e.g. "fr\\SomeAssembly.resources.dll" -->
|
||||
<BootManifestResourceType Condition="'%(Extension)' == '.dll'">assembly</BootManifestResourceType>
|
||||
<BootManifestResourceType Condition="'%(Extension)' == '.pdb'">pdb</BootManifestResourceType>
|
||||
<BootManifestResourceType Condition="'%(_BlazorCopyLocalPaths.Culture)' == '' AND '%(_BlazorCopyLocalPaths.Extension)' == '.dll'">assembly</BootManifestResourceType>
|
||||
<BootManifestResourceType Condition="'%(_BlazorCopyLocalPaths.Culture)' != '' AND '%(_BlazorCopyLocalPaths.Extension)' == '.dll'">satellite</BootManifestResourceType>
|
||||
<BootManifestResourceName>%(_BlazorCopyLocalPaths.DestinationSubDirectory)%(FileName)%(Extension)</BootManifestResourceName>
|
||||
<TargetOutputPath>$(_BlazorRuntimeBinOutputPath)%(_BlazorCopyLocalPaths.DestinationSubDirectory)%(FileName)%(Extension)</TargetOutputPath>
|
||||
</_BlazorOutputWithTargetPath>
|
||||
|
||||
<_BlazorOutputWithTargetPath Include="@(ReferenceSatellitePaths)">
|
||||
<Culture>$([System.String]::Copy('%(ReferenceSatellitePaths.DestinationSubDirectory)').Trim('\').Trim('/'))</Culture>
|
||||
<BootManifestResourceType>satellite</BootManifestResourceType>
|
||||
<BootManifestResourceName>%(ReferenceSatellitePaths.DestinationSubDirectory)%(FileName)%(Extension)</BootManifestResourceName>
|
||||
<TargetOutputPath>$(_BlazorRuntimeBinOutputPath)%(ReferenceSatellitePaths.DestinationSubDirectory)%(FileName)%(Extension)</TargetOutputPath>
|
||||
</_BlazorOutputWithTargetPath>
|
||||
|
||||
<_BlazorOutputWithTargetPath Include="@(_BlazorResolvedAssembly)">
|
||||
<BootManifestResourceType Condition="'%(Extension)' == '.dll'">assembly</BootManifestResourceType>
|
||||
<BootManifestResourceType Condition="'%(Extension)' == '.pdb'">pdb</BootManifestResourceType>
|
||||
<BootManifestResourceType Condition="'%(_BlazorCopyLocalPaths.Extension)' == '.pdb'">pdb</BootManifestResourceType>
|
||||
<BootManifestResourceName>%(_BlazorCopyLocalPaths.DestinationSubDirectory)%(FileName)%(Extension)</BootManifestResourceName>
|
||||
<TargetOutputPath>$(_BlazorRuntimeBinOutputPath)%(_BlazorCopyLocalPaths.DestinationSubDirectory)%(FileName)%(Extension)</TargetOutputPath>
|
||||
</_BlazorOutputWithTargetPath>
|
||||
|
||||
<_BlazorOutputWithTargetPath Include="@(_DotNetWasmRuntimeFile)">
|
||||
<TargetOutputPath>$(_BlazorRuntimeWasmOutputPath)%(FileName)%(Extension)</TargetOutputPath>
|
||||
<BootManifestResourceType>runtime</BootManifestResourceType>
|
||||
<BootManifestResourceName>%(FileName)%(Extension)</BootManifestResourceName>
|
||||
<TargetOutputPath>$(_BlazorRuntimeBinOutputPath)%(FileName)%(Extension)</TargetOutputPath>
|
||||
</_BlazorOutputWithTargetPath>
|
||||
|
||||
<_BlazorOutputWithTargetPath Include="@(_BlazorJSFile)">
|
||||
<TargetOutputPath>$(_BaseBlazorRuntimeOutputPath)%(FileName)%(Extension)</TargetOutputPath>
|
||||
</_BlazorOutputWithTargetPath>
|
||||
</ItemGroup>
|
||||
|
||||
|
|
@ -101,42 +132,6 @@
|
|||
<ItemGroup Condition="'$(BlazorEnableDebugging)' != 'true'">
|
||||
<_BlazorOutputWithTargetPath Remove="@(_BlazorOutputWithTargetPath)" Condition="'%(Extension)' == '.pdb'" />
|
||||
</ItemGroup>
|
||||
|
||||
<!--
|
||||
The following itemgroup attempts to extend the set to include satellite assemblies.
|
||||
The mechanism behind this (or whether it's correct) is a bit unclear so
|
||||
https://github.com/dotnet/aspnetcore/issues/18951 tracks the need for follow-up.
|
||||
-->
|
||||
<ItemGroup>
|
||||
<!--
|
||||
ReferenceCopyLocalPaths includes all files that are part of the build out with CopyLocalLockFileAssemblies on.
|
||||
Remove assemblies that are inputs to calculating the assembly closure. Instead use the resolved outputs, since it is the minimal set.
|
||||
-->
|
||||
<_BlazorCopyLocalPaths Include="@(ReferenceCopyLocalPaths)" Condition="'%(Extension)' == '.dll'" />
|
||||
<_BlazorCopyLocalPaths Remove="@(_BlazorManagedRuntimeAssemby)" Condition="'%(Extension)' == '.dll'" />
|
||||
|
||||
<_BlazorOutputWithTargetPath Include="@(_BlazorCopyLocalPaths)">
|
||||
<BootManifestResourceType Condition="'%(Extension)' == '.dll'">assembly</BootManifestResourceType>
|
||||
<BootManifestResourceType Condition="'%(Extension)' == '.pdb'">pdb</BootManifestResourceType>
|
||||
<BootManifestResourceName>%(_BlazorCopyLocalPaths.DestinationSubDirectory)%(FileName)%(Extension)</BootManifestResourceName>
|
||||
<TargetOutputPath>$(_BlazorRuntimeBinOutputPath)%(_BlazorCopyLocalPaths.DestinationSubDirectory)%(FileName)%(Extension)</TargetOutputPath>
|
||||
</_BlazorOutputWithTargetPath>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<_DotNetWasmRuntimeFile Include="$(ComponentsWebAssemblyRuntimePath)*" />
|
||||
<_BlazorOutputWithTargetPath Include="@(_DotNetWasmRuntimeFile)">
|
||||
<TargetOutputPath>$(_BlazorRuntimeWasmOutputPath)%(FileName)%(Extension)</TargetOutputPath>
|
||||
<BootManifestResourceType>runtime</BootManifestResourceType>
|
||||
<BootManifestResourceName>%(FileName)%(Extension)</BootManifestResourceName>
|
||||
</_BlazorOutputWithTargetPath>
|
||||
|
||||
<_BlazorJSFile Include="$(_BlazorJSPath)" />
|
||||
<_BlazorJSFile Include="$(_BlazorJSMapPath)" Condition="Exists('$(_BlazorJSMapPath)')" />
|
||||
<_BlazorOutputWithTargetPath Include="@(_BlazorJSFile)">
|
||||
<TargetOutputPath>$(_BaseBlazorRuntimeOutputPath)%(FileName)%(Extension)</TargetOutputPath>
|
||||
</_BlazorOutputWithTargetPath>
|
||||
</ItemGroup>
|
||||
</Target>
|
||||
|
||||
<!--
|
||||
|
|
@ -218,7 +213,7 @@
|
|||
<Target
|
||||
Name="_LinkBlazorApplication"
|
||||
Inputs="$(ProjectAssetsFile);
|
||||
@(_BlazorManagedRuntimeAssemby);
|
||||
@(_BlazorManagedRuntimeAssembly);
|
||||
@(BlazorLinkerDescriptor);
|
||||
$(MSBuildAllProjects)"
|
||||
Outputs="$(_BlazorLinkerOutputCache)">
|
||||
|
|
@ -277,7 +272,7 @@
|
|||
Name="_ResolveBlazorRuntimeDependencies"
|
||||
Inputs="$(ProjectAssetsFile);
|
||||
@(IntermediateAssembly);
|
||||
@(_BlazorManagedRuntimeAssemby)"
|
||||
@(_BlazorManagedRuntimeAssembly)"
|
||||
Outputs="$(_BlazorApplicationAssembliesCacheFile)">
|
||||
|
||||
<!--
|
||||
|
|
@ -287,7 +282,7 @@
|
|||
-->
|
||||
<ResolveBlazorRuntimeDependencies
|
||||
EntryPoint="@(IntermediateAssembly)"
|
||||
ApplicationDependencies="@(_BlazorManagedRuntimeAssemby)"
|
||||
ApplicationDependencies="@(_BlazorManagedRuntimeAssembly)"
|
||||
WebAssemblyBCLAssemblies="@(_WebAssemblyBCLAssembly)">
|
||||
|
||||
<Output TaskParameter="Dependencies" ItemName="_BlazorResolvedAssembly" />
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
<SourceId>$(PackageId)</SourceId>
|
||||
<ContentRoot>$([MSBuild]::NormalizeDirectory('$(TargetDir)wwwroot\'))</ContentRoot>
|
||||
<BasePath>$(StaticWebAssetBasePath)</BasePath>
|
||||
<RelativePath>$([System.String]::Copy('%(_BlazorOutputWithTargetPath.TargetOutputPath)').Replace('\','/').Replace('dist/',''))</RelativePath>
|
||||
<RelativePath>$([System.String]::Copy('%(_BlazorOutputWithTargetPath.TargetOutputPath)').Replace('\','/'))</RelativePath>
|
||||
</StaticWebAsset>
|
||||
|
||||
<StaticWebAsset Remove="@(StaticWebAsset)" Condition="'$(BlazorEnableDebugging)' != 'true' and '%(SourceType)' == '' and '%(Extension)' == '.pdb'" />
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
using static Microsoft.AspNetCore.Components.WebAssembly.Build.WebAssemblyRuntimePackage;
|
||||
|
|
@ -133,9 +134,19 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Build
|
|||
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "_bin", "Microsoft.CodeAnalysis.CSharp.dll");
|
||||
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "_bin", "fr", "Microsoft.CodeAnalysis.CSharp.resources.dll"); // Verify satellite assemblies are present in the build output.
|
||||
|
||||
var bootJsonPath = Path.Combine(buildOutputDirectory, "wwwroot", "_framework", "blazor.boot.json");
|
||||
Assert.FileContains(result, bootJsonPath, "\"Microsoft.CodeAnalysis.CSharp.dll\"");
|
||||
Assert.FileContains(result, bootJsonPath, "\"fr\\/Microsoft.CodeAnalysis.CSharp.resources.dll\"");
|
||||
var bootJson = JsonSerializer.Deserialize<GenerateBlazorBootJson.BootJsonData>(
|
||||
File.ReadAllText(Path.Combine(project.DirectoryPath, buildOutputDirectory, "wwwroot", "_framework", "blazor.boot.json")),
|
||||
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
||||
|
||||
var satelliteResources = bootJson.resources.satelliteResources;
|
||||
Assert.NotNull(satelliteResources);
|
||||
|
||||
Assert.Contains("es-ES", satelliteResources.Keys);
|
||||
Assert.Contains("es-ES/classlibrarywithsatelliteassemblies.resources.dll", satelliteResources["es-ES"].Keys);
|
||||
Assert.Contains("fr", satelliteResources.Keys);
|
||||
Assert.Contains("fr/Microsoft.CodeAnalysis.CSharp.resources.dll", satelliteResources["fr"].Keys);
|
||||
Assert.Contains("ja", satelliteResources.Keys);
|
||||
Assert.Contains("ja/standalone.resources.dll", satelliteResources["ja"].Keys);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,12 +2,12 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.Serialization.Json;
|
||||
using Microsoft.Build.Framework;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
using BootJsonData = Microsoft.AspNetCore.Components.WebAssembly.Build.GenerateBlazorBootJson.BootJsonData;
|
||||
using ResourceType = Microsoft.AspNetCore.Components.WebAssembly.Build.GenerateBlazorBootJson.ResourceType;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.WebAssembly.Build
|
||||
{
|
||||
|
|
@ -23,24 +23,42 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Build
|
|||
Resources = new[]
|
||||
{
|
||||
CreateResourceTaskItem(
|
||||
ResourceType.assembly,
|
||||
"assembly",
|
||||
name: "My.Assembly1.ext", // Can specify filename with no dir
|
||||
fileHash: "abcdefghikjlmnopqrstuvwxyz"),
|
||||
|
||||
CreateResourceTaskItem(
|
||||
ResourceType.assembly,
|
||||
"assembly",
|
||||
name: "dir\\My.Assembly2.ext2", // Can specify Windows-style path
|
||||
fileHash: "012345678901234567890123456789"),
|
||||
|
||||
CreateResourceTaskItem(
|
||||
ResourceType.pdb,
|
||||
"pdb",
|
||||
name: "otherdir/SomePdb.pdb", // Can specify Linux-style path
|
||||
fileHash: "pdbhashpdbhashpdbhash"),
|
||||
|
||||
CreateResourceTaskItem(
|
||||
ResourceType.runtime,
|
||||
"runtime",
|
||||
name: "some-runtime-file", // Can specify path with no extension
|
||||
fileHash: "runtimehashruntimehash")
|
||||
fileHash: "runtimehashruntimehash"),
|
||||
|
||||
CreateResourceTaskItem(
|
||||
"satellite",
|
||||
name: "en-GB\\satellite-assembly1.ext",
|
||||
fileHash: "hashsatelliteassembly1",
|
||||
("Culture", "en-GB")),
|
||||
|
||||
CreateResourceTaskItem(
|
||||
"satellite",
|
||||
name: "fr/satellite-assembly2.dll",
|
||||
fileHash: "hashsatelliteassembly2",
|
||||
("Culture", "fr")),
|
||||
|
||||
CreateResourceTaskItem(
|
||||
"satellite",
|
||||
name: "en-GB\\satellite-assembly3.ext",
|
||||
fileHash: "hashsatelliteassembly3",
|
||||
("Culture", "en-GB")),
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -52,28 +70,49 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Build
|
|||
// Assert
|
||||
var parsedContent = ParseBootData(stream);
|
||||
Assert.Equal("MyEntrypointAssembly", parsedContent.entryAssembly);
|
||||
Assert.Collection(parsedContent.resources.Keys,
|
||||
resourceListKey =>
|
||||
|
||||
var resources = parsedContent.resources.assembly;
|
||||
Assert.Equal(2, resources.Count);
|
||||
Assert.Equal("sha256-abcdefghikjlmnopqrstuvwxyz", resources["My.Assembly1.ext"]);
|
||||
Assert.Equal("sha256-012345678901234567890123456789", resources["dir/My.Assembly2.ext2"]); // Paths are converted to use URL-style separators
|
||||
|
||||
resources = parsedContent.resources.pdb;
|
||||
Assert.Single(resources);
|
||||
Assert.Equal("sha256-pdbhashpdbhashpdbhash", resources["otherdir/SomePdb.pdb"]);
|
||||
|
||||
resources = parsedContent.resources.runtime;
|
||||
Assert.Single(resources);
|
||||
Assert.Equal("sha256-runtimehashruntimehash", resources["some-runtime-file"]);
|
||||
|
||||
var satelliteResources = parsedContent.resources.satelliteResources;
|
||||
Assert.Collection(
|
||||
satelliteResources.OrderBy(kvp => kvp.Key),
|
||||
kvp =>
|
||||
{
|
||||
var resources = parsedContent.resources[resourceListKey];
|
||||
Assert.Equal(ResourceType.assembly, resourceListKey);
|
||||
Assert.Equal(2, resources.Count);
|
||||
Assert.Equal("sha256-abcdefghikjlmnopqrstuvwxyz", resources["My.Assembly1.ext"]);
|
||||
Assert.Equal("sha256-012345678901234567890123456789", resources["dir/My.Assembly2.ext2"]); // Paths are converted to use URL-style separators
|
||||
Assert.Equal("en-GB", kvp.Key);
|
||||
Assert.Collection(
|
||||
kvp.Value.OrderBy(item => item.Key),
|
||||
item =>
|
||||
{
|
||||
Assert.Equal("en-GB/satellite-assembly1.ext", item.Key);
|
||||
Assert.Equal("sha256-hashsatelliteassembly1", item.Value);
|
||||
},
|
||||
item =>
|
||||
{
|
||||
Assert.Equal("en-GB/satellite-assembly3.ext", item.Key);
|
||||
Assert.Equal("sha256-hashsatelliteassembly3", item.Value);
|
||||
});
|
||||
},
|
||||
resourceListKey =>
|
||||
kvp =>
|
||||
{
|
||||
var resources = parsedContent.resources[resourceListKey];
|
||||
Assert.Equal(ResourceType.pdb, resourceListKey);
|
||||
Assert.Single(resources);
|
||||
Assert.Equal("sha256-pdbhashpdbhashpdbhash", resources["otherdir/SomePdb.pdb"]);
|
||||
},
|
||||
resourceListKey =>
|
||||
{
|
||||
var resources = parsedContent.resources[resourceListKey];
|
||||
Assert.Equal(ResourceType.runtime, resourceListKey);
|
||||
Assert.Single(resources);
|
||||
Assert.Equal("sha256-runtimehashruntimehash", resources["some-runtime-file"]);
|
||||
Assert.Equal("fr", kvp.Key);
|
||||
Assert.Collection(
|
||||
kvp.Value.OrderBy(item => item.Key),
|
||||
item =>
|
||||
{
|
||||
Assert.Equal("fr/satellite-assembly2.dll", item.Key);
|
||||
Assert.Equal("sha256-hashsatelliteassembly2", item.Value);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -137,12 +176,20 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Build
|
|||
return (BootJsonData)serializer.ReadObject(stream);
|
||||
}
|
||||
|
||||
private static ITaskItem CreateResourceTaskItem(ResourceType type, string name, string fileHash)
|
||||
private static ITaskItem CreateResourceTaskItem(string type, string name, string fileHash, params (string key, string value)[] values)
|
||||
{
|
||||
var mock = new Mock<ITaskItem>();
|
||||
mock.Setup(m => m.GetMetadata("BootManifestResourceType")).Returns(type.ToString());
|
||||
mock.Setup(m => m.GetMetadata("BootManifestResourceType")).Returns(type);
|
||||
mock.Setup(m => m.GetMetadata("BootManifestResourceName")).Returns(name);
|
||||
mock.Setup(m => m.GetMetadata("FileHash")).Returns(fileHash);
|
||||
|
||||
if (values != null)
|
||||
{
|
||||
foreach (var (key, value) in values)
|
||||
{
|
||||
mock.Setup(m => m.GetMetadata(key)).Returns(value);
|
||||
}
|
||||
}
|
||||
return mock.Object;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,123 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="Hello" xml:space="preserve">
|
||||
<value>Hola</value>
|
||||
</data>
|
||||
</root>
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="Hello" xml:space="preserve">
|
||||
<value>Konnichiwa</value>
|
||||
</data>
|
||||
</root>
|
||||
|
|
@ -16,33 +16,35 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
|
|||
// entrypoint result to the JS caller. There's no requirement to do that today, and if we
|
||||
// do change this it will be non-breaking.
|
||||
public static void InvokeEntrypoint(string assemblyName, string[] args)
|
||||
=> InvokeEntrypoint(assemblyName, args, SatelliteResourcesLoader.Init());
|
||||
|
||||
internal static async void InvokeEntrypoint(string assemblyName, string[] args, SatelliteResourcesLoader satelliteResourcesLoader)
|
||||
{
|
||||
object entrypointResult;
|
||||
try
|
||||
{
|
||||
// Emscripten sets up the culture for the application based on the user's language preferences.
|
||||
// Before we execute the app's entry point, load satellite assemblies for this culture.
|
||||
// We'll allow users to configure their app culture as part of MainAsync. Loading satellite assemblies
|
||||
// for the configured culture will happen as part of WebAssemblyHost.RunAsync.
|
||||
await satelliteResourcesLoader.LoadCurrentCultureResourcesAsync();
|
||||
|
||||
var assembly = Assembly.Load(assemblyName);
|
||||
var entrypoint = FindUnderlyingEntrypoint(assembly);
|
||||
var @params = entrypoint.GetParameters().Length == 1 ? new object[] { args ?? Array.Empty<string>() } : new object[] { };
|
||||
entrypointResult = entrypoint.Invoke(null, @params);
|
||||
|
||||
var result = entrypoint.Invoke(null, @params);
|
||||
if (result is Task resultTask)
|
||||
{
|
||||
// In the default case, this Task is backed by the WebAssemblyHost.RunAsync that never completes.
|
||||
// Awaiting it is allows catching any exception thrown by user code in MainAsync.
|
||||
await resultTask;
|
||||
}
|
||||
}
|
||||
catch (Exception syncException)
|
||||
{
|
||||
HandleStartupException(syncException);
|
||||
return;
|
||||
}
|
||||
|
||||
// If the entrypoint is async, handle async exceptions in the same way that we would
|
||||
// have handled sync ones
|
||||
if (entrypointResult is Task entrypointTask)
|
||||
{
|
||||
entrypointTask.ContinueWith(task =>
|
||||
{
|
||||
if (task.Exception != null)
|
||||
{
|
||||
HandleStartupException(task.Exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static MethodBase FindUnderlyingEntrypoint(Assembly assembly)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,109 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Components.WebAssembly.Services;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
|
||||
{
|
||||
internal class SatelliteResourcesLoader
|
||||
{
|
||||
internal const string GetSatelliteAssemblies = "window.Blazor._internal.getSatelliteAssemblies";
|
||||
internal const string ReadSatelliteAssemblies = "window.Blazor._internal.readSatelliteAssemblies";
|
||||
|
||||
private readonly WebAssemblyJSRuntimeInvoker _invoker;
|
||||
private CultureInfo _previousCulture;
|
||||
|
||||
// For unit testing.
|
||||
internal SatelliteResourcesLoader(WebAssemblyJSRuntimeInvoker invoker)
|
||||
{
|
||||
_invoker = invoker;
|
||||
}
|
||||
|
||||
public static SatelliteResourcesLoader Instance { get; private set; }
|
||||
|
||||
public static SatelliteResourcesLoader Init()
|
||||
{
|
||||
Debug.Assert(Instance is null, "Init should not be called multiple times.");
|
||||
|
||||
Instance = new SatelliteResourcesLoader(WebAssemblyJSRuntimeInvoker.Instance);
|
||||
return Instance;
|
||||
}
|
||||
|
||||
public ValueTask LoadCurrentCultureResourcesAsync()
|
||||
{
|
||||
if (_previousCulture != CultureInfo.CurrentCulture)
|
||||
{
|
||||
_previousCulture = CultureInfo.CurrentCulture;
|
||||
return LoadSatelliteAssembliesForCurrentCultureAsync();
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
protected virtual async ValueTask LoadSatelliteAssembliesForCurrentCultureAsync()
|
||||
{
|
||||
var culturesToLoad = GetCultures(CultureInfo.CurrentCulture);
|
||||
|
||||
if (culturesToLoad.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Now that we know the cultures we care about, let WebAssemblyResourceLoader (in JavaScript) load these
|
||||
// assemblies. We effectively want to resovle a Task<byte[][]> but there is no way to express this
|
||||
// using interop. We'll instead do this in two parts:
|
||||
// getSatelliteAssemblies resolves when all satellite assemblies to be loaded in .NET are fetched and available in memory.
|
||||
var count = (int)await _invoker.InvokeUnmarshalled<string[], object, object, Task<object>>(
|
||||
GetSatelliteAssemblies,
|
||||
culturesToLoad.ToArray(),
|
||||
null,
|
||||
null);
|
||||
|
||||
if (count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// readSatelliteAssemblies resolves the assembly bytes
|
||||
var assemblies = _invoker.InvokeUnmarshalled<object, object, object, object[]>(
|
||||
ReadSatelliteAssemblies,
|
||||
null,
|
||||
null,
|
||||
null);
|
||||
|
||||
for (var i = 0; i < assemblies.Length; i++)
|
||||
{
|
||||
Assembly.Load((byte[])assemblies[i]);
|
||||
}
|
||||
}
|
||||
|
||||
internal static List<string> GetCultures(CultureInfo cultureInfo)
|
||||
{
|
||||
var culturesToLoad = new List<string>();
|
||||
|
||||
// Once WASM is ready, we have to use .NET's assembly loading to load additional assemblies.
|
||||
// First calculate all possible cultures that the application might want to load. We do this by
|
||||
// starting from the current culture and walking up the graph of parents.
|
||||
// At the end of the the walk, we'll have a list of culture names that look like
|
||||
// [ "fr-FR", "fr" ]
|
||||
while (cultureInfo != null && cultureInfo != CultureInfo.InvariantCulture)
|
||||
{
|
||||
culturesToLoad.Add(cultureInfo.Name);
|
||||
|
||||
if (cultureInfo.Parent == cultureInfo)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
cultureInfo = cultureInfo.Parent;
|
||||
}
|
||||
|
||||
return culturesToLoad;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -58,6 +58,8 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
|
|||
/// </summary>
|
||||
public IServiceProvider Services => _scope.ServiceProvider;
|
||||
|
||||
internal SatelliteResourcesLoader SatelliteResourcesLoader { get; set; } = SatelliteResourcesLoader.Instance;
|
||||
|
||||
/// <summary>
|
||||
/// Disposes the host asynchronously.
|
||||
/// </summary>
|
||||
|
|
@ -132,6 +134,10 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
|
|||
await _renderer.AddComponentAsync(rootComponent.ComponentType, rootComponent.Selector);
|
||||
}
|
||||
|
||||
// Users may want to configure the culture based on some ambient state such as local storage, url etc.
|
||||
// If they have changed the culture since the initial load, fetch satellite assemblies for this selection.
|
||||
await SatelliteResourcesLoader.LoadCurrentCultureResourcesAsync();
|
||||
|
||||
await tcs.Task;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")]
|
||||
|
|
@ -2,12 +2,15 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Runtime.Loader;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Components.WebAssembly.Services;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
|
||||
|
|
@ -33,7 +36,7 @@ static " + returnType + @" Main(" + paramsDecl + @")
|
|||
}", out var didMainExecute);
|
||||
|
||||
// Act
|
||||
EntrypointInvoker.InvokeEntrypoint(assembly.FullName, new string[] { });
|
||||
EntrypointInvoker.InvokeEntrypoint(assembly.FullName, new string[] { }, new TestSatelliteResourcesLoader());
|
||||
|
||||
// Assert
|
||||
Assert.True(didMainExecute());
|
||||
|
|
@ -63,7 +66,7 @@ static async Task" + returnTypeGenericParam + @" Main(" + paramsDecl + @")
|
|||
// Act/Assert 1: Waits for task
|
||||
// The fact that we're not blocking here proves that we're not executing the
|
||||
// metadata-declared entrypoint, as that would block
|
||||
EntrypointInvoker.InvokeEntrypoint(assembly.FullName, new string[] { });
|
||||
EntrypointInvoker.InvokeEntrypoint(assembly.FullName, new string[] { }, new TestSatelliteResourcesLoader());
|
||||
Assert.False(didMainExecute());
|
||||
|
||||
// Act/Assert 2: Continues
|
||||
|
|
@ -88,7 +91,7 @@ public static void Main()
|
|||
// to handle the exception. We can't assert about what it does here, because that
|
||||
// would involve capturing console output, which isn't safe in unit tests. Instead
|
||||
// we'll check this in E2E tests.
|
||||
EntrypointInvoker.InvokeEntrypoint(assembly.FullName, new string[] { });
|
||||
EntrypointInvoker.InvokeEntrypoint(assembly.FullName, new string[] { }, new TestSatelliteResourcesLoader());
|
||||
Assert.True(didMainExecute());
|
||||
}
|
||||
|
||||
|
|
@ -107,7 +110,7 @@ public static async Task Main()
|
|||
}", out var didMainExecute);
|
||||
|
||||
// Act/Assert 1: Waits for task
|
||||
EntrypointInvoker.InvokeEntrypoint(assembly.FullName, new string[] { });
|
||||
EntrypointInvoker.InvokeEntrypoint(assembly.FullName, new string[] { }, new TestSatelliteResourcesLoader());
|
||||
Assert.False(didMainExecute());
|
||||
|
||||
// Act/Assert 2: Continues
|
||||
|
|
@ -149,5 +152,15 @@ namespace SomeApp
|
|||
|
||||
return assembly;
|
||||
}
|
||||
|
||||
private class TestSatelliteResourcesLoader : SatelliteResourcesLoader
|
||||
{
|
||||
internal TestSatelliteResourcesLoader()
|
||||
: base(WebAssemblyJSRuntimeInvoker.Instance)
|
||||
{
|
||||
}
|
||||
|
||||
protected override ValueTask LoadSatelliteAssembliesForCurrentCultureAsync() => default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,114 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Components.WebAssembly.Services;
|
||||
using Microsoft.AspNetCore.Testing;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
using static Microsoft.AspNetCore.Components.WebAssembly.Hosting.SatelliteResourcesLoader;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
|
||||
{
|
||||
public class SatelliteResourcesLoaderTest
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("fr-FR", new[] { "fr-FR", "fr" })]
|
||||
[InlineData("tzm-Latn-DZ", new[] { "tzm-Latn-DZ", "tzm-Latn", "tzm" })]
|
||||
public void GetCultures_ReturnsCultureClosure(string cultureName, string[] expected)
|
||||
{
|
||||
// Arrange
|
||||
var culture = new CultureInfo(cultureName);
|
||||
|
||||
// Act
|
||||
var actual = SatelliteResourcesLoader.GetCultures(culture);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LoadCurrentCultureResourcesAsync_ReadsAssemblies()
|
||||
{
|
||||
// Arrange
|
||||
using var cultureReplacer = new CultureReplacer("en-GB");
|
||||
var invoker = new Mock<WebAssemblyJSRuntimeInvoker>();
|
||||
invoker.Setup(i => i.InvokeUnmarshalled<string[], object, object, Task<object>>(GetSatelliteAssemblies, new[] { "en-GB", "en" }, null, null))
|
||||
.Returns(Task.FromResult<object>(1))
|
||||
.Verifiable();
|
||||
|
||||
invoker.Setup(i => i.InvokeUnmarshalled<object, object, object, object[]>(ReadSatelliteAssemblies, null, null, null))
|
||||
.Returns(new object[] { File.ReadAllBytes(GetType().Assembly.Location) })
|
||||
.Verifiable();
|
||||
|
||||
var loader = new SatelliteResourcesLoader(invoker.Object);
|
||||
|
||||
// Act
|
||||
await loader.LoadCurrentCultureResourcesAsync();
|
||||
|
||||
// Assert
|
||||
invoker.Verify();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LoadCurrentCultureResourcesAsync_DoesNotReadAssembliesWhenThereAreNone()
|
||||
{
|
||||
// Arrange
|
||||
using var cultureReplacer = new CultureReplacer("en-GB");
|
||||
var invoker = new Mock<WebAssemblyJSRuntimeInvoker>();
|
||||
invoker.Setup(i => i.InvokeUnmarshalled<string[], object, object, Task<object>>(GetSatelliteAssemblies, new[] { "en-GB", "en" }, null, null))
|
||||
.Returns(Task.FromResult<object>(0))
|
||||
.Verifiable();
|
||||
|
||||
var loader = new SatelliteResourcesLoader(invoker.Object);
|
||||
|
||||
// Act
|
||||
await loader.LoadCurrentCultureResourcesAsync();
|
||||
|
||||
// Assert
|
||||
invoker.Verify(i => i.InvokeUnmarshalled<object, object, object, object[]>(ReadSatelliteAssemblies, null, null, null), Times.Never());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LoadCurrentCultureResourcesAsync_AttemptsToReadAssemblies_IfCultureIsChangedBetweenInvocation()
|
||||
{
|
||||
// Arrange
|
||||
using var cultureReplacer = new CultureReplacer("en-GB");
|
||||
var invoker = new Mock<WebAssemblyJSRuntimeInvoker>();
|
||||
invoker.Setup(i => i.InvokeUnmarshalled<string[], object, object, Task<object>>(GetSatelliteAssemblies, It.IsAny<string[]>(), null, null))
|
||||
.Returns(Task.FromResult<object>(0))
|
||||
.Verifiable();
|
||||
|
||||
var loader = new SatelliteResourcesLoader(invoker.Object);
|
||||
|
||||
// Act
|
||||
await loader.LoadCurrentCultureResourcesAsync();
|
||||
CultureInfo.CurrentCulture = new CultureInfo("fr-fr");
|
||||
await loader.LoadCurrentCultureResourcesAsync();
|
||||
|
||||
invoker.Verify(i => i.InvokeUnmarshalled<object, object, object, Task<object>>(GetSatelliteAssemblies, It.IsAny<string[]>(), null, null),
|
||||
Times.Exactly(2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LoadCurrentCultureResourcesAsync_NoOps_WhenInvokedSecondTime_WithSameCulture()
|
||||
{
|
||||
// Arrange
|
||||
using var cultureReplacer = new CultureReplacer("en-GB");
|
||||
var invoker = new Mock<WebAssemblyJSRuntimeInvoker>();
|
||||
invoker.Setup(i => i.InvokeUnmarshalled<string[], object, object, Task<object>>(GetSatelliteAssemblies, It.IsAny<string[]>(), null, null))
|
||||
.Returns(Task.FromResult<object>(0));;
|
||||
|
||||
var loader = new SatelliteResourcesLoader(invoker.Object);
|
||||
|
||||
// Act
|
||||
await loader.LoadCurrentCultureResourcesAsync();
|
||||
await loader.LoadCurrentCultureResourcesAsync();
|
||||
|
||||
invoker.Verify(i => i.InvokeUnmarshalled<object, object, object, Task<object>>(GetSatelliteAssemblies, It.IsAny<string[]>(), null, null),
|
||||
Times.Once());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@
|
|||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Components.WebAssembly.Services;
|
||||
using Microsoft.AspNetCore.Testing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Xunit;
|
||||
|
|
@ -20,6 +21,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
|
|||
// Arrange
|
||||
var builder = new WebAssemblyHostBuilder(new TestWebAssemblyJSRuntimeInvoker());
|
||||
var host = builder.Build();
|
||||
host.SatelliteResourcesLoader = new TestSatelliteResourcesLoader();
|
||||
|
||||
var cts = new CancellationTokenSource();
|
||||
|
||||
|
|
@ -38,6 +40,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
|
|||
// Arrange
|
||||
var builder = new WebAssemblyHostBuilder(new TestWebAssemblyJSRuntimeInvoker());
|
||||
var host = builder.Build();
|
||||
host.SatelliteResourcesLoader = new TestSatelliteResourcesLoader();
|
||||
|
||||
var cts = new CancellationTokenSource();
|
||||
var task = host.RunAsyncCore(cts.Token);
|
||||
|
|
@ -59,6 +62,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
|
|||
var builder = new WebAssemblyHostBuilder(new TestWebAssemblyJSRuntimeInvoker());
|
||||
builder.Services.AddSingleton<DisposableService>();
|
||||
var host = builder.Build();
|
||||
host.SatelliteResourcesLoader = new TestSatelliteResourcesLoader();
|
||||
|
||||
var disposable = host.Services.GetRequiredService<DisposableService>();
|
||||
|
||||
|
|
@ -87,5 +91,15 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
|
|||
return new ValueTask(Task.CompletedTask);
|
||||
}
|
||||
}
|
||||
|
||||
private class TestSatelliteResourcesLoader : SatelliteResourcesLoader
|
||||
{
|
||||
internal TestSatelliteResourcesLoader()
|
||||
: base(WebAssemblyJSRuntimeInvoker.Instance)
|
||||
{
|
||||
}
|
||||
|
||||
protected override ValueTask LoadSatelliteAssembliesForCurrentCultureAsync() => default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,54 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using BasicTestApp;
|
||||
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
|
||||
using Microsoft.AspNetCore.Components.E2ETests.Tests;
|
||||
using Microsoft.AspNetCore.E2ETesting;
|
||||
using OpenQA.Selenium;
|
||||
using OpenQA.Selenium.Support.UI;
|
||||
using TestServer;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
||||
{
|
||||
// For now this is limited to server-side execution because we don't have the ability to set the
|
||||
// culture in client-side Blazor.
|
||||
public class ServerGlobalizationTest : GlobalizationTest<BasicTestAppServerSiteFixture<InternationalizationStartup>>
|
||||
{
|
||||
public ServerGlobalizationTest(
|
||||
BrowserFixture browserFixture,
|
||||
BasicTestAppServerSiteFixture<InternationalizationStartup> serverFixture,
|
||||
ITestOutputHelper output)
|
||||
: base(browserFixture, serverFixture, output)
|
||||
{
|
||||
}
|
||||
|
||||
protected override TimeSpan UtcOffset => TimeZoneInfo.Local.BaseUtcOffset;
|
||||
|
||||
protected override void InitializeAsyncCore()
|
||||
{
|
||||
Navigate(ServerPathBase);
|
||||
Browser.MountTestComponent<CulturePicker>();
|
||||
Browser.Exists(By.Id("culture-selector"));
|
||||
}
|
||||
|
||||
protected override void SetCulture(string culture)
|
||||
{
|
||||
var selector = new SelectElement(Browser.FindElement(By.Id("culture-selector")));
|
||||
selector.SelectByValue(culture);
|
||||
|
||||
// Click the link to return back to the test page
|
||||
Browser.Exists(By.ClassName("return-from-culture-setter")).Click();
|
||||
|
||||
// That should have triggered a page load, so wait for the main test selector to come up.
|
||||
Browser.MountTestComponent<GlobalizationBindCases>();
|
||||
Browser.Exists(By.Id("globalization-cases"));
|
||||
|
||||
var cultureDisplay = Browser.Exists(By.Id("culture-name-display"));
|
||||
Assert.Equal($"Culture is: {culture}", cultureDisplay.Text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -13,11 +13,9 @@ using Xunit.Abstractions;
|
|||
|
||||
namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
||||
{
|
||||
// For now this is limited to server-side execution because we don't have the ability to set the
|
||||
// culture in client-side Blazor.
|
||||
public class LocalizationTest : ServerTestBase<BasicTestAppServerSiteFixture<InternationalizationStartup>>
|
||||
public class ServerLocalizationTest : ServerTestBase<BasicTestAppServerSiteFixture<InternationalizationStartup>>
|
||||
{
|
||||
public LocalizationTest(
|
||||
public ServerLocalizationTest(
|
||||
BrowserFixture browserFixture,
|
||||
BasicTestAppServerSiteFixture<InternationalizationStartup> serverFixture,
|
||||
ITestOutputHelper output)
|
||||
|
|
@ -3,36 +3,31 @@
|
|||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using BasicTestApp;
|
||||
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure;
|
||||
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
|
||||
using Microsoft.AspNetCore.E2ETesting;
|
||||
using OpenQA.Selenium;
|
||||
using OpenQA.Selenium.Support.UI;
|
||||
using TestServer;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
||||
namespace Microsoft.AspNetCore.Components.E2ETests.Tests
|
||||
{
|
||||
// For now this is limited to server-side execution because we don't have the ability to set the
|
||||
// culture in client-side Blazor.
|
||||
public class GlobalizationTest : ServerTestBase<BasicTestAppServerSiteFixture<InternationalizationStartup>>
|
||||
public abstract class GlobalizationTest<TServerFixture> : ServerTestBase<TServerFixture>
|
||||
where TServerFixture : ServerFixture
|
||||
{
|
||||
public GlobalizationTest(
|
||||
BrowserFixture browserFixture,
|
||||
BasicTestAppServerSiteFixture<InternationalizationStartup> serverFixture,
|
||||
ITestOutputHelper output)
|
||||
public GlobalizationTest(BrowserFixture browserFixture, TServerFixture serverFixture, ITestOutputHelper output)
|
||||
: base(browserFixture, serverFixture, output)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void InitializeAsyncCore()
|
||||
{
|
||||
Navigate(ServerPathBase);
|
||||
Browser.MountTestComponent<CulturePicker>();
|
||||
Browser.Exists(By.Id("culture-selector"));
|
||||
}
|
||||
protected abstract void SetCulture(string culture);
|
||||
|
||||
/// <summary>
|
||||
/// Blazor Server and these tests will use the application's UtcOffset when calculating DateTimeOffset when
|
||||
/// an offset is not explicitly specified. Blazor WASM always calculates DateTimeOffsets as Utc.
|
||||
/// We'll use <see cref="UtcOffset"/> to express this difference in calculating expected values in these tests.
|
||||
/// </summary>
|
||||
protected abstract TimeSpan UtcOffset { get; }
|
||||
|
||||
[Theory]
|
||||
[InlineData("en-US")]
|
||||
|
|
@ -74,11 +69,11 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
// datetimeoffset
|
||||
input = Browser.FindElement(By.Id("input_type_text_datetimeoffset"));
|
||||
display = Browser.FindElement(By.Id("input_type_text_datetimeoffset_value"));
|
||||
Browser.Equal(new DateTimeOffset(new DateTime(1985, 3, 4)).ToString(cultureInfo), () => display.Text);
|
||||
Browser.Equal(new DateTimeOffset(new DateTime(1985, 3, 4), UtcOffset).ToString(cultureInfo), () => display.Text);
|
||||
|
||||
input.ReplaceText(new DateTimeOffset(new DateTime(2000, 1, 2)).ToString(cultureInfo));
|
||||
input.ReplaceText(new DateTimeOffset(new DateTime(2000, 1, 2), UtcOffset).ToString(cultureInfo));
|
||||
input.SendKeys("\t");
|
||||
Browser.Equal(new DateTimeOffset(new DateTime(2000, 1, 2)).ToString(cultureInfo), () => display.Text);
|
||||
Browser.Equal(new DateTimeOffset(new DateTime(2000, 1, 2), UtcOffset).ToString(cultureInfo), () => display.Text);
|
||||
}
|
||||
|
||||
// The logic is different for verifying culture-invariant fields. The problem is that the logic for what
|
||||
|
|
@ -145,13 +140,13 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
input = Browser.FindElement(By.Id("input_type_date_datetimeoffset"));
|
||||
display = Browser.FindElement(By.Id("input_type_date_datetimeoffset_value"));
|
||||
extraInput = Browser.FindElement(By.Id("input_type_date_datetimeoffset_extrainput"));
|
||||
Browser.Equal(new DateTimeOffset(new DateTime(1985, 3, 4)).ToString(cultureInfo), () => display.Text);
|
||||
Browser.Equal(new DateTimeOffset(new DateTime(1985, 3, 4)).ToString("yyyy-MM-dd", CultureInfo.InvariantCulture), () => input.GetAttribute("value"));
|
||||
Browser.Equal(new DateTimeOffset(new DateTime(1985, 3, 4), UtcOffset).ToString(cultureInfo), () => display.Text);
|
||||
Browser.Equal(new DateTimeOffset(new DateTime(1985, 3, 4), UtcOffset).ToString("yyyy-MM-dd", CultureInfo.InvariantCulture), () => input.GetAttribute("value"));
|
||||
|
||||
extraInput.ReplaceText(new DateTimeOffset(new DateTime(2000, 1, 2)).ToString(cultureInfo));
|
||||
extraInput.ReplaceText(new DateTimeOffset(new DateTime(2000, 1, 2), UtcOffset).ToString(cultureInfo));
|
||||
extraInput.SendKeys("\t");
|
||||
Browser.Equal(new DateTimeOffset(new DateTime(2000, 1, 2)).ToString(cultureInfo), () => display.Text);
|
||||
Browser.Equal(new DateTimeOffset(new DateTime(2000, 1, 2)).ToString("yyyy-MM-dd", CultureInfo.InvariantCulture), () => input.GetAttribute("value"));
|
||||
Browser.Equal(new DateTimeOffset(new DateTime(2000, 1, 2), UtcOffset).ToString(cultureInfo), () => display.Text);
|
||||
Browser.Equal(new DateTimeOffset(new DateTime(2000, 1, 2), UtcOffset).ToString("yyyy-MM-dd", CultureInfo.InvariantCulture), () => input.GetAttribute("value"));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
|
|
@ -214,29 +209,13 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
input = Browser.FindElement(By.Id("inputdate_datetimeoffset"));
|
||||
display = Browser.FindElement(By.Id("inputdate_datetimeoffset_value"));
|
||||
extraInput = Browser.FindElement(By.Id("inputdate_datetimeoffset_extrainput"));
|
||||
Browser.Equal(new DateTimeOffset(new DateTime(1985, 3, 4)).ToString(cultureInfo), () => display.Text);
|
||||
Browser.Equal(new DateTimeOffset(new DateTime(1985, 3, 4)).ToString("yyyy-MM-dd", CultureInfo.InvariantCulture), () => input.GetAttribute("value"));
|
||||
Browser.Equal(new DateTimeOffset(new DateTime(1985, 3, 4), UtcOffset).ToString(cultureInfo), () => display.Text);
|
||||
Browser.Equal(new DateTimeOffset(new DateTime(1985, 3, 4), UtcOffset).ToString("yyyy-MM-dd", CultureInfo.InvariantCulture), () => input.GetAttribute("value"));
|
||||
|
||||
extraInput.ReplaceText(new DateTimeOffset(new DateTime(2000, 1, 2)).ToString(cultureInfo));
|
||||
extraInput.ReplaceText(new DateTimeOffset(new DateTime(2000, 1, 2), UtcOffset).ToString(cultureInfo));
|
||||
extraInput.SendKeys("\t");
|
||||
Browser.Equal(new DateTimeOffset(new DateTime(2000, 1, 2)).ToString(cultureInfo), () => display.Text);
|
||||
Browser.Equal(new DateTimeOffset(new DateTime(2000, 1, 2)).ToString("yyyy-MM-dd", CultureInfo.InvariantCulture), () => input.GetAttribute("value"));
|
||||
}
|
||||
|
||||
private void SetCulture(string culture)
|
||||
{
|
||||
var selector = new SelectElement(Browser.FindElement(By.Id("culture-selector")));
|
||||
selector.SelectByValue(culture);
|
||||
|
||||
// Click the link to return back to the test page
|
||||
Browser.Exists(By.ClassName("return-from-culture-setter")).Click();
|
||||
|
||||
// That should have triggered a page load, so wait for the main test selector to come up.
|
||||
Browser.MountTestComponent<GlobalizationBindCases>();
|
||||
Browser.Exists(By.Id("globalization-cases"));
|
||||
|
||||
var cultureDisplay = Browser.Exists(By.Id("culture-name-display"));
|
||||
Assert.Equal($"Culture is: {culture}", cultureDisplay.Text);
|
||||
Browser.Equal(new DateTimeOffset(new DateTime(2000, 1, 2), UtcOffset).ToString(cultureInfo), () => display.Text);
|
||||
Browser.Equal(new DateTimeOffset(new DateTime(2000, 1, 2), UtcOffset).ToString("yyyy-MM-dd", CultureInfo.InvariantCulture), () => input.GetAttribute("value"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using BasicTestApp;
|
||||
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
|
||||
using Microsoft.AspNetCore.Components.E2ETests.Tests;
|
||||
using Microsoft.AspNetCore.E2ETesting;
|
||||
using OpenQA.Selenium;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
||||
{
|
||||
// For now this is limited to server-side execution because we don't have the ability to set the
|
||||
// culture in client-side Blazor.
|
||||
public class WebAssemblyGlobalizationTest : GlobalizationTest<ToggleExecutionModeServerFixture<Program>>
|
||||
{
|
||||
public WebAssemblyGlobalizationTest(
|
||||
BrowserFixture browserFixture,
|
||||
ToggleExecutionModeServerFixture<Program> serverFixture,
|
||||
ITestOutputHelper output)
|
||||
: base(browserFixture, serverFixture, output)
|
||||
{
|
||||
}
|
||||
|
||||
protected override TimeSpan UtcOffset => TimeSpan.Zero;
|
||||
|
||||
protected override void SetCulture(string culture)
|
||||
{
|
||||
Navigate($"{ServerPathBase}/?culture={culture}", noReload: false);
|
||||
|
||||
// That should have triggered a page load, so wait for the main test selector to come up.
|
||||
Browser.MountTestComponent<GlobalizationBindCases>();
|
||||
Browser.Exists(By.Id("globalization-cases"));
|
||||
|
||||
var cultureDisplay = Browser.Exists(By.Id("culture-name-display"));
|
||||
Assert.Equal($"Culture is: {culture}", cultureDisplay.Text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using BasicTestApp;
|
||||
using Microsoft.AspNetCore.Components.E2ETest;
|
||||
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure;
|
||||
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
|
||||
using Microsoft.AspNetCore.E2ETesting;
|
||||
using OpenQA.Selenium;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
||||
{
|
||||
public class WebAssemblyLocalizationTest : ServerTestBase<ToggleExecutionModeServerFixture<Program>>
|
||||
{
|
||||
public WebAssemblyLocalizationTest(
|
||||
BrowserFixture browserFixture,
|
||||
ToggleExecutionModeServerFixture<Program> serverFixture,
|
||||
ITestOutputHelper output)
|
||||
: base(browserFixture, serverFixture, output)
|
||||
{
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("en-US", "Hello!")]
|
||||
[InlineData("fr-FR", "Bonjour!")]
|
||||
public void CanSetCultureAndReadLocalizedResources(string culture, string message)
|
||||
{
|
||||
Navigate($"{ServerPathBase}/?culture={culture}", noReload: false);
|
||||
|
||||
Browser.MountTestComponent<LocalizedText>();
|
||||
|
||||
var cultureDisplay = Browser.Exists(By.Id("culture-name-display"));
|
||||
Assert.Equal($"Culture is: {culture}", cultureDisplay.Text);
|
||||
|
||||
var messageDisplay = Browser.FindElement(By.Id("message-display"));
|
||||
Assert.Equal(message, messageDisplay.Text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -7,7 +7,9 @@ using System.Linq;
|
|||
using System.Net.Http;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
using BasicTestApp.AuthTest;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||
using Microsoft.AspNetCore.Components.WebAssembly.Http;
|
||||
|
|
@ -23,9 +25,6 @@ namespace BasicTestApp
|
|||
{
|
||||
await SimulateErrorsIfNeededForTest();
|
||||
|
||||
// We want the culture to be en-US so that the tests for bind can work consistently.
|
||||
CultureInfo.CurrentCulture = new CultureInfo("en-US");
|
||||
|
||||
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
||||
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Create("WEBASSEMBLY")))
|
||||
|
|
@ -55,7 +54,36 @@ namespace BasicTestApp
|
|||
return new PrependMessageLoggerFactory("Custom logger", originalLogger);
|
||||
});
|
||||
|
||||
await builder.Build().RunAsync();
|
||||
var host = builder.Build();
|
||||
ConfigureCulture(host);
|
||||
|
||||
await host.RunAsync();
|
||||
}
|
||||
|
||||
private static void ConfigureCulture(WebAssemblyHost host)
|
||||
{
|
||||
// In the absence of a specified value, we want the culture to be en-US so that the tests for bind can work consistently.
|
||||
var culture = new CultureInfo("en-US");
|
||||
|
||||
Uri uri = null;
|
||||
try
|
||||
{
|
||||
uri = new Uri(host.Services.GetService<NavigationManager>().Uri);
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
// Some of our tests set this application up incorrectly so that querying NavigationManager.Uri throws.
|
||||
}
|
||||
|
||||
if (uri != null && HttpUtility.ParseQueryString(uri.Query)["culture"] is string cultureName)
|
||||
{
|
||||
culture = new CultureInfo(cultureName);
|
||||
}
|
||||
|
||||
// CultureInfo.CurrentCulture is async-scoped and will not affect the culture in sibling scopes.
|
||||
// Use CultureInfo.DefaultThreadCurrentCulture instead to modify the application's default scope.
|
||||
CultureInfo.DefaultThreadCurrentCulture = culture;
|
||||
CultureInfo.DefaultThreadCurrentUICulture = culture;
|
||||
}
|
||||
|
||||
// Supports E2E tests in StartupErrorNotificationTest
|
||||
|
|
|
|||
Loading…
Reference in New Issue