diff --git a/src/Components/Web.JS/dist/Release/blazor.webassembly.js b/src/Components/Web.JS/dist/Release/blazor.webassembly.js index 879365839533..faf8cd3c32cb 100644 --- a/src/Components/Web.JS/dist/Release/blazor.webassembly.js +++ b/src/Components/Web.JS/dist/Release/blazor.webassembly.js @@ -1 +1 @@ -!function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=44)}([,,,,,function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),n(25),n(18);var r=n(26),o=n(13),i={},a=!1;function u(e,t,n){var o=i[e];o||(o=i[e]=new r.BrowserRenderer(e)),o.attachRootComponentToLogicalElement(n,t)}t.attachRootComponentToLogicalElement=u,t.attachRootComponentToElement=function(e,t,n){var r=document.querySelector(e);if(!r)throw new Error("Could not find any element matching selector '"+e+"'.");u(n||0,o.toLogicalElement(r,!0),t)},t.renderBatch=function(e,t){var n=i[e];if(!n)throw new Error("There is no browser renderer with ID "+e+".");for(var r=t.arrayRangeReader,o=t.updatedComponents(),u=r.values(o),s=r.count(o),c=t.referenceFrames(),l=r.values(c),f=t.diffReader,d=0;d0&&o[o.length-1])&&(6===i[0]||2===i[0])){a=0;continue}if(3===i[0]&&(!o||i[1]>o[0]&&i[1]0&&!t)throw new Error("New logical elements must start empty, or allowExistingContents must be true");return e[r]=[],e}function u(e,t,n){var i=e;if(e instanceof Comment&&(c(i)&&c(i).length>0))throw new Error("Not implemented: inserting non-empty logical container");if(s(i))throw new Error("Not implemented: moving existing logical children");var a=c(t);if(n0;)e(r,0);var i=r;i.parentNode.removeChild(i)},t.getLogicalParent=s,t.getLogicalSiblingEnd=function(e){return e[i]||null},t.getLogicalChild=function(e,t){return c(e)[t]},t.isSvgElement=function(e){return"http://www.w3.org/2000/svg"===l(e).namespaceURI},t.getLogicalChildrenArray=c,t.permuteLogicalChildren=function(e,t){var n=c(e);t.forEach(function(e){e.moveRangeStart=n[e.fromSiblingIndex],e.moveRangeEnd=function e(t){if(t instanceof Element)return t;var n=f(t);if(n)return n.previousSibling;var r=s(t);return r instanceof Element?r.lastChild:e(r)}(e.moveRangeStart)}),t.forEach(function(t){var r=t.moveToBeforeMarker=document.createComment("marker"),o=n[t.toSiblingIndex+1];o?o.parentNode.insertBefore(r,o):d(r,e)}),t.forEach(function(e){for(var t=e.moveToBeforeMarker,n=t.parentNode,r=e.moveRangeStart,o=e.moveRangeEnd,i=r;i;){var a=i.nextSibling;if(n.insertBefore(i,t),i===o)break;i=a}n.removeChild(t)}),t.forEach(function(e){n[e.toSiblingIndex]=e.moveRangeStart})},t.getClosestDomElement=l},,,,function(e,t,n){"use strict";var r;!function(e){window.DotNet=e;var t=[],n={},r={},o=1,i=null;function a(e){t.push(e)}function u(e,t,n,r){var o=c();if(o.invokeDotNetFromJS){var i=JSON.stringify(r,m),a=o.invokeDotNetFromJS(e,t,n,i);return a?f(a):null}throw new Error("The current dispatcher does not support synchronous calls from JS to .NET. Use invokeMethodAsync instead.")}function s(e,t,r,i){if(e&&r)throw new Error("For instance method calls, assemblyName should be null. Received '"+e+"'.");var a=o++,u=new Promise(function(e,t){n[a]={resolve:e,reject:t}});try{var s=JSON.stringify(i,m);c().beginInvokeDotNetFromJS(a,e,t,r,s)}catch(e){l(a,!1,e)}return u}function c(){if(null!==i)return i;throw new Error("No .NET call dispatcher has been set.")}function l(e,t,r){if(!n.hasOwnProperty(e))throw new Error("There is no pending async call with ID "+e+".");var o=n[e];delete n[e],t?o.resolve(r):o.reject(r)}function f(e){return e?JSON.parse(e,function(e,n){return t.reduce(function(t,n){return n(e,t)},n)}):null}function d(e){return e instanceof Error?e.message+"\n"+e.stack:e?e.toString():"null"}function p(e){if(r.hasOwnProperty(e))return r[e];var t,n=window,o="window";if(e.split(".").forEach(function(e){if(!(e in n))throw new Error("Could not find '"+e+"' in '"+o+"'.");t=n,n=n[e],o+="."+e}),n instanceof Function)return n=n.bind(t),r[e]=n,n;throw new Error("The value '"+o+"' is not a function.")}e.attachDispatcher=function(e){i=e},e.attachReviver=a,e.invokeMethod=function(e,t){for(var n=[],r=2;r0&&o[o.length-1])&&(6===i[0]||2===i[0])){a=0;continue}if(3===i[0]&&(!o||i[1]>o[0]&&i[1]0&&o[o.length-1])&&(6===i[0]||2===i[0])){a=0;continue}if(3===i[0]&&(!o||i[1]>o[0]&&i[1]0)&&!(r=i.next()).done;)a.push(r.value)}catch(e){o={error:e}}finally{try{r&&!r.done&&(n=i.return)&&n.call(i)}finally{if(o)throw o.error}}return a};Object.defineProperty(t,"__esModule",{value:!0}),n(17),n(24);var a=n(18),u=n(45),s=n(5),c=n(47),l=n(33),f=n(19),d=n(48),p=n(49),h=n(50),m=!1;function v(e){return r(this,void 0,void 0,function(){var e,t,n,l,v,y=this;return o(this,function(b){switch(b.label){case 0:if(m)throw new Error("Blazor has already started.");return m=!0,f.setEventDispatcher(function(e,t){return DotNet.invokeMethodAsync("Microsoft.AspNetCore.Components.WebAssembly","DispatchEvent",e,JSON.stringify(t))}),e=a.setPlatform(u.monoPlatform),window.Blazor.platform=e,window.Blazor._internal.renderBatch=function(e,t){s.renderBatch(e,new c.SharedMemoryRenderBatch(t))},window.Blazor._internal.navigationManager.listenForNavigationEvents(function(e,t){return r(y,void 0,void 0,function(){return o(this,function(n){switch(n.label){case 0:return[4,DotNet.invokeMethodAsync("Microsoft.AspNetCore.Components.WebAssembly","NotifyLocationChanged",e,t)];case 1:return n.sent(),[2]}})})}),[4,h.BootConfigResult.initAsync()];case 1:return t=b.sent(),[4,Promise.all([d.WebAssemblyResourceLoader.initAsync(t.bootConfig),p.WebAssemblyConfigLoader.initAsync(t)])];case 2:n=i.apply(void 0,[b.sent(),1]),l=n[0],b.label=3;case 3:return b.trys.push([3,5,,6]),[4,e.start(l)];case 4:return b.sent(),[3,6];case 5:throw v=b.sent(),new Error("Failed to start platform. Reason: "+v);case 6:return e.callEntryPoint(l.bootConfig.entryAssembly),[2]}})})}window.Blazor.start=v,l.shouldAutoStart()&&v().catch(function(e){"undefined"!=typeof Module&&Module.printErr?Module.printErr(e):console.error(e)})},function(e,t,n){"use strict";var r=this&&this.__awaiter||function(e,t,n,r){return new(n||(n=Promise))(function(o,i){function a(e){try{s(r.next(e))}catch(e){i(e)}}function u(e){try{s(r.throw(e))}catch(e){i(e)}}function s(e){e.done?o(e.value):new n(function(t){t(e.value)}).then(a,u)}s((r=r.apply(e,t||[])).next())})},o=this&&this.__generator||function(e,t){var n,r,o,i,a={label:0,sent:function(){if(1&o[0])throw o[1];return o[1]},trys:[],ops:[]};return i={next:u(0),throw:u(1),return:u(2)},"function"==typeof Symbol&&(i[Symbol.iterator]=function(){return this}),i;function u(i){return function(u){return function(i){if(n)throw new TypeError("Generator is already executing.");for(;a;)try{if(n=1,r&&(o=2&i[0]?r.return:i[0]?r.throw||((o=r.return)&&o.call(r),0):r.next)&&!(o=o.call(r,i[1])).done)return o;switch(r=0,o&&(i=[2&i[0],o.value]),i[0]){case 0:case 1:o=i;break;case 4:return a.label++,{value:i[1],done:!1};case 5:a.label++,r=i[1],i=[0];continue;case 7:i=a.ops.pop(),a.trys.pop();continue;default:if(!(o=(o=a.trys).length>0&&o[o.length-1])&&(6===i[0]||2===i[0])){a=0;continue}if(3===i[0]&&(!o||i[1]>o[0]&&i[1]>2,r=Module.HEAPU32[n+1];if(r>f)throw new Error("Cannot read uint64 with high order part "+r+", because the result would exceed Number.MAX_SAFE_INTEGER.");return r*l+Module.HEAPU32[n]},readFloatField:function(e,t){return Module.getValue(e+(t||0),"float")},readObjectField:function(e,t){return Module.getValue(e+(t||0),"i32")},readStringField:function(e,n){var r=Module.getValue(e+(n||0),"i32");return 0===r?null:t.monoPlatform.toJavaScriptString(r)},readStructField:function(e,t){return e+(t||0)}};var d=document.createElement("a");function p(e){return e+12}function h(e,t,n){var r="["+e+"] "+t+":"+n;return BINDING.bind_static_method(r)}function m(e,t){return r(this,void 0,void 0,function(){var n,r;return o(this,function(o){switch(o.label){case 0:if("function"!=typeof WebAssembly.instantiateStreaming)return[3,4];o.label=1;case 1:return o.trys.push([1,3,,4]),[4,WebAssembly.instantiateStreaming(e.response,t)];case 2:return[2,o.sent().instance];case 3:return n=o.sent(),console.info("Streaming compilation failed. Falling back to ArrayBuffer instantiation. ",n),[3,4];case 4:return[4,e.response.then(function(e){return e.arrayBuffer()})];case 5:return r=o.sent(),[4,WebAssembly.instantiate(r,t)];case 6:return[2,o.sent().instance]}})})}},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=window.chrome&&navigator.userAgent.indexOf("Edge")<0,o=!1;function i(){return o&&r}t.hasDebuggingEnabled=i,t.attachDebuggerHotkey=function(e){var t=navigator.platform.match(/^Mac/i)?"Cmd":"Alt";i()&&console.info("Debugging hotkey: Shift+"+t+"+D (when application has focus)"),o=!!e.bootConfig.resources.pdb,document.addEventListener("keydown",function(e){var t;e.shiftKey&&(e.metaKey||e.altKey)&&"KeyD"===e.code&&(o?r?((t=document.createElement("a")).href="_framework/debug?url="+encodeURIComponent(location.href),t.target="_blank",t.rel="noopener noreferrer",t.click()):console.error("Currently, only Edge(Chromium) or Chrome is supported for debugging."):console.error("Cannot start debugging, because the application was not compiled with debugging enabled."))})}},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=n(18),o=function(){function e(e){this.batchAddress=e,this.arrayRangeReader=i,this.arrayBuilderSegmentReader=a,this.diffReader=u,this.editReader=s,this.frameReader=c}return e.prototype.updatedComponents=function(){return r.platform.readStructField(this.batchAddress,0)},e.prototype.referenceFrames=function(){return r.platform.readStructField(this.batchAddress,i.structLength)},e.prototype.disposedComponentIds=function(){return r.platform.readStructField(this.batchAddress,2*i.structLength)},e.prototype.disposedEventHandlerIds=function(){return r.platform.readStructField(this.batchAddress,3*i.structLength)},e.prototype.updatedComponentsEntry=function(e,t){return l(e,t,u.structLength)},e.prototype.referenceFramesEntry=function(e,t){return l(e,t,c.structLength)},e.prototype.disposedComponentIdsEntry=function(e,t){var n=l(e,t,4);return r.platform.readInt32Field(n)},e.prototype.disposedEventHandlerIdsEntry=function(e,t){var n=l(e,t,8);return r.platform.readUint64Field(n)},e}();t.SharedMemoryRenderBatch=o;var i={structLength:8,values:function(e){return r.platform.readObjectField(e,0)},count:function(e){return r.platform.readInt32Field(e,4)}},a={structLength:12,values:function(e){var t=r.platform.readObjectField(e,0),n=r.platform.getObjectFieldsBaseAddress(t);return r.platform.readObjectField(n,0)},offset:function(e){return r.platform.readInt32Field(e,4)},count:function(e){return r.platform.readInt32Field(e,8)}},u={structLength:4+a.structLength,componentId:function(e){return r.platform.readInt32Field(e,0)},edits:function(e){return r.platform.readStructField(e,4)},editsEntry:function(e,t){return l(e,t,s.structLength)}},s={structLength:20,editType:function(e){return r.platform.readInt32Field(e,0)},siblingIndex:function(e){return r.platform.readInt32Field(e,4)},newTreeIndex:function(e){return r.platform.readInt32Field(e,8)},moveToSiblingIndex:function(e){return r.platform.readInt32Field(e,8)},removedAttributeName:function(e){return r.platform.readStringField(e,16)}},c={structLength:36,frameType:function(e){return r.platform.readInt16Field(e,4)},subtreeLength:function(e){return r.platform.readInt32Field(e,8)},elementReferenceCaptureId:function(e){return r.platform.readStringField(e,16)},componentId:function(e){return r.platform.readInt32Field(e,12)},elementName:function(e){return r.platform.readStringField(e,16)},textContent:function(e){return r.platform.readStringField(e,16)},markupContent:function(e){return r.platform.readStringField(e,16)},attributeName:function(e){return r.platform.readStringField(e,16)},attributeValue:function(e){return r.platform.readStringField(e,24)},attributeEventHandlerId:function(e){return r.platform.readUint64Field(e,8)}};function l(e,t,n){return r.platform.getArrayEntryPtr(e,t,n)}},function(e,t,n){"use strict";var r=this&&this.__awaiter||function(e,t,n,r){return new(n||(n=Promise))(function(o,i){function a(e){try{s(r.next(e))}catch(e){i(e)}}function u(e){try{s(r.throw(e))}catch(e){i(e)}}function s(e){e.done?o(e.value):new n(function(t){t(e.value)}).then(a,u)}s((r=r.apply(e,t||[])).next())})},o=this&&this.__generator||function(e,t){var n,r,o,i,a={label:0,sent:function(){if(1&o[0])throw o[1];return o[1]},trys:[],ops:[]};return i={next:u(0),throw:u(1),return:u(2)},"function"==typeof Symbol&&(i[Symbol.iterator]=function(){return this}),i;function u(i){return function(u){return function(i){if(n)throw new TypeError("Generator is already executing.");for(;a;)try{if(n=1,r&&(o=2&i[0]?r.return:i[0]?r.throw||((o=r.return)&&o.call(r),0):r.next)&&!(o=o.call(r,i[1])).done)return o;switch(r=0,o&&(i=[2&i[0],o.value]),i[0]){case 0:case 1:o=i;break;case 4:return a.label++,{value:i[1],done:!1};case 5:a.label++,r=i[1],i=[0];continue;case 7:i=a.ops.pop(),a.trys.pop();continue;default:if(!(o=(o=a.trys).length>0&&o[o.length-1])&&(6===i[0]||2===i[0])){a=0;continue}if(3===i[0]&&(!o||i[1]>o[0]&&i[1]0&&o[o.length-1])&&(6===i[0]||2===i[0])){a=0;continue}if(3===i[0]&&(!o||i[1]>o[0]&&i[1]0&&o[o.length-1])&&(6===i[0]||2===i[0])){a=0;continue}if(3===i[0]&&(!o||i[1]>o[0]&&i[1]0&&o[o.length-1])&&(6===a[0]||2===a[0])){i=0;continue}if(3===a[0]&&(!o||a[1]>o[0]&&a[1]0&&!t)throw new Error("New logical elements must start empty, or allowExistingContents must be true");return e[r]=[],e}function u(e,t,n){var a=e;if(e instanceof Comment&&(c(a)&&c(a).length>0))throw new Error("Not implemented: inserting non-empty logical container");if(s(a))throw new Error("Not implemented: moving existing logical children");var i=c(t);if(n0;)e(r,0);var a=r;a.parentNode.removeChild(a)},t.getLogicalParent=s,t.getLogicalSiblingEnd=function(e){return e[a]||null},t.getLogicalChild=function(e,t){return c(e)[t]},t.isSvgElement=function(e){return"http://www.w3.org/2000/svg"===l(e).namespaceURI},t.getLogicalChildrenArray=c,t.permuteLogicalChildren=function(e,t){var n=c(e);t.forEach(function(e){e.moveRangeStart=n[e.fromSiblingIndex],e.moveRangeEnd=function e(t){if(t instanceof Element)return t;var n=f(t);if(n)return n.previousSibling;var r=s(t);return r instanceof Element?r.lastChild:e(r)}(e.moveRangeStart)}),t.forEach(function(t){var r=t.moveToBeforeMarker=document.createComment("marker"),o=n[t.toSiblingIndex+1];o?o.parentNode.insertBefore(r,o):d(r,e)}),t.forEach(function(e){for(var t=e.moveToBeforeMarker,n=t.parentNode,r=e.moveRangeStart,o=e.moveRangeEnd,a=r;a;){var i=a.nextSibling;if(n.insertBefore(a,t),a===o)break;a=i}n.removeChild(t)}),t.forEach(function(e){n[e.toSiblingIndex]=e.moveRangeStart})},t.getClosestDomElement=l},,,,function(e,t,n){"use strict";var r;!function(e){window.DotNet=e;var t=[],n={},r={},o=1,a=null;function i(e){t.push(e)}function u(e,t,n,r){var o=c();if(o.invokeDotNetFromJS){var a=JSON.stringify(r,m),i=o.invokeDotNetFromJS(e,t,n,a);return i?f(i):null}throw new Error("The current dispatcher does not support synchronous calls from JS to .NET. Use invokeMethodAsync instead.")}function s(e,t,r,a){if(e&&r)throw new Error("For instance method calls, assemblyName should be null. Received '"+e+"'.");var i=o++,u=new Promise(function(e,t){n[i]={resolve:e,reject:t}});try{var s=JSON.stringify(a,m);c().beginInvokeDotNetFromJS(i,e,t,r,s)}catch(e){l(i,!1,e)}return u}function c(){if(null!==a)return a;throw new Error("No .NET call dispatcher has been set.")}function l(e,t,r){if(!n.hasOwnProperty(e))throw new Error("There is no pending async call with ID "+e+".");var o=n[e];delete n[e],t?o.resolve(r):o.reject(r)}function f(e){return e?JSON.parse(e,function(e,n){return t.reduce(function(t,n){return n(e,t)},n)}):null}function d(e){return e instanceof Error?e.message+"\n"+e.stack:e?e.toString():"null"}function p(e){if(r.hasOwnProperty(e))return r[e];var t,n=window,o="window";if(e.split(".").forEach(function(e){if(!(e in n))throw new Error("Could not find '"+e+"' in '"+o+"'.");t=n,n=n[e],o+="."+e}),n instanceof Function)return n=n.bind(t),r[e]=n,n;throw new Error("The value '"+o+"' is not a function.")}e.attachDispatcher=function(e){a=e},e.attachReviver=i,e.invokeMethod=function(e,t){for(var n=[],r=2;r0&&o[o.length-1])&&(6===a[0]||2===a[0])){i=0;continue}if(3===a[0]&&(!o||a[1]>o[0]&&a[1]0&&o[o.length-1])&&(6===a[0]||2===a[0])){i=0;continue}if(3===a[0]&&(!o||a[1]>o[0]&&a[1]0)&&!(r=a.next()).done;)i.push(r.value)}catch(e){o={error:e}}finally{try{r&&!r.done&&(n=a.return)&&n.call(a)}finally{if(o)throw o.error}}return i};Object.defineProperty(t,"__esModule",{value:!0}),n(17),n(24);var i=n(18),u=n(45),s=n(5),c=n(47),l=n(33),f=n(19),d=n(48),p=n(49),h=n(50),m=!1;function v(e){return r(this,void 0,void 0,function(){var e,t,n,l,v,y=this;return o(this,function(b){switch(b.label){case 0:if(m)throw new Error("Blazor has already started.");return m=!0,f.setEventDispatcher(function(e,t){return DotNet.invokeMethodAsync("Microsoft.AspNetCore.Components.WebAssembly","DispatchEvent",e,JSON.stringify(t))}),e=i.setPlatform(u.monoPlatform),window.Blazor.platform=e,window.Blazor._internal.renderBatch=function(e,t){s.renderBatch(e,new c.SharedMemoryRenderBatch(t))},window.Blazor._internal.navigationManager.listenForNavigationEvents(function(e,t){return r(y,void 0,void 0,function(){return o(this,function(n){switch(n.label){case 0:return[4,DotNet.invokeMethodAsync("Microsoft.AspNetCore.Components.WebAssembly","NotifyLocationChanged",e,t)];case 1:return n.sent(),[2]}})})}),[4,h.BootConfigResult.initAsync()];case 1:return t=b.sent(),[4,Promise.all([d.WebAssemblyResourceLoader.initAsync(t.bootConfig),p.WebAssemblyConfigLoader.initAsync(t)])];case 2:n=a.apply(void 0,[b.sent(),1]),l=n[0],b.label=3;case 3:return b.trys.push([3,5,,6]),[4,e.start(l)];case 4:return b.sent(),[3,6];case 5:throw v=b.sent(),new Error("Failed to start platform. Reason: "+v);case 6:return e.callEntryPoint(l.bootConfig.entryAssembly),[2]}})})}window.Blazor.start=v,l.shouldAutoStart()&&v().catch(function(e){"undefined"!=typeof Module&&Module.printErr?Module.printErr(e):console.error(e)})},function(e,t,n){"use strict";var r=this&&this.__awaiter||function(e,t,n,r){return new(n||(n=Promise))(function(o,a){function i(e){try{s(r.next(e))}catch(e){a(e)}}function u(e){try{s(r.throw(e))}catch(e){a(e)}}function s(e){e.done?o(e.value):new n(function(t){t(e.value)}).then(i,u)}s((r=r.apply(e,t||[])).next())})},o=this&&this.__generator||function(e,t){var n,r,o,a,i={label:0,sent:function(){if(1&o[0])throw o[1];return o[1]},trys:[],ops:[]};return a={next:u(0),throw:u(1),return:u(2)},"function"==typeof Symbol&&(a[Symbol.iterator]=function(){return this}),a;function u(a){return function(u){return function(a){if(n)throw new TypeError("Generator is already executing.");for(;i;)try{if(n=1,r&&(o=2&a[0]?r.return:a[0]?r.throw||((o=r.return)&&o.call(r),0):r.next)&&!(o=o.call(r,a[1])).done)return o;switch(r=0,o&&(a=[2&a[0],o.value]),a[0]){case 0:case 1:o=a;break;case 4:return i.label++,{value:a[1],done:!1};case 5:i.label++,r=a[1],a=[0];continue;case 7:a=i.ops.pop(),i.trys.pop();continue;default:if(!(o=(o=i.trys).length>0&&o[o.length-1])&&(6===a[0]||2===a[0])){i=0;continue}if(3===a[0]&&(!o||a[1]>o[0]&&a[1]>2,r=Module.HEAPU32[n+1];if(r>f)throw new Error("Cannot read uint64 with high order part "+r+", because the result would exceed Number.MAX_SAFE_INTEGER.");return r*l+Module.HEAPU32[n]},readFloatField:function(e,t){return Module.getValue(e+(t||0),"float")},readObjectField:function(e,t){return Module.getValue(e+(t||0),"i32")},readStringField:function(e,n){var r=Module.getValue(e+(n||0),"i32");return 0===r?null:t.monoPlatform.toJavaScriptString(r)},readStructField:function(e,t){return e+(t||0)}};var d=document.createElement("a");function p(e){return e+12}function h(e,t,n){var r="["+e+"] "+t+":"+n;return BINDING.bind_static_method(r)}function m(e,t){return r(this,void 0,void 0,function(){var n,r;return o(this,function(o){switch(o.label){case 0:if("function"!=typeof WebAssembly.instantiateStreaming)return[3,4];o.label=1;case 1:return o.trys.push([1,3,,4]),[4,WebAssembly.instantiateStreaming(e.response,t)];case 2:return[2,o.sent().instance];case 3:return n=o.sent(),console.info("Streaming compilation failed. Falling back to ArrayBuffer instantiation. ",n),[3,4];case 4:return[4,e.response.then(function(e){return e.arrayBuffer()})];case 5:return r=o.sent(),[4,WebAssembly.instantiate(r,t)];case 6:return[2,o.sent().instance]}})})}},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=window.chrome&&navigator.userAgent.indexOf("Edge")<0,o=!1;function a(){return o&&r}t.hasDebuggingEnabled=a,t.attachDebuggerHotkey=function(e){var t=navigator.platform.match(/^Mac/i)?"Cmd":"Alt";a()&&console.info("Debugging hotkey: Shift+"+t+"+D (when application has focus)"),o=!!e.bootConfig.resources.pdb,document.addEventListener("keydown",function(e){var t;e.shiftKey&&(e.metaKey||e.altKey)&&"KeyD"===e.code&&(o?r?((t=document.createElement("a")).href="_framework/debug?url="+encodeURIComponent(location.href),t.target="_blank",t.rel="noopener noreferrer",t.click()):console.error("Currently, only Edge(Chromium) or Chrome is supported for debugging."):console.error("Cannot start debugging, because the application was not compiled with debugging enabled."))})}},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=n(18),o=function(){function e(e){this.batchAddress=e,this.arrayRangeReader=a,this.arrayBuilderSegmentReader=i,this.diffReader=u,this.editReader=s,this.frameReader=c}return e.prototype.updatedComponents=function(){return r.platform.readStructField(this.batchAddress,0)},e.prototype.referenceFrames=function(){return r.platform.readStructField(this.batchAddress,a.structLength)},e.prototype.disposedComponentIds=function(){return r.platform.readStructField(this.batchAddress,2*a.structLength)},e.prototype.disposedEventHandlerIds=function(){return r.platform.readStructField(this.batchAddress,3*a.structLength)},e.prototype.updatedComponentsEntry=function(e,t){return l(e,t,u.structLength)},e.prototype.referenceFramesEntry=function(e,t){return l(e,t,c.structLength)},e.prototype.disposedComponentIdsEntry=function(e,t){var n=l(e,t,4);return r.platform.readInt32Field(n)},e.prototype.disposedEventHandlerIdsEntry=function(e,t){var n=l(e,t,8);return r.platform.readUint64Field(n)},e}();t.SharedMemoryRenderBatch=o;var a={structLength:8,values:function(e){return r.platform.readObjectField(e,0)},count:function(e){return r.platform.readInt32Field(e,4)}},i={structLength:12,values:function(e){var t=r.platform.readObjectField(e,0),n=r.platform.getObjectFieldsBaseAddress(t);return r.platform.readObjectField(n,0)},offset:function(e){return r.platform.readInt32Field(e,4)},count:function(e){return r.platform.readInt32Field(e,8)}},u={structLength:4+i.structLength,componentId:function(e){return r.platform.readInt32Field(e,0)},edits:function(e){return r.platform.readStructField(e,4)},editsEntry:function(e,t){return l(e,t,s.structLength)}},s={structLength:20,editType:function(e){return r.platform.readInt32Field(e,0)},siblingIndex:function(e){return r.platform.readInt32Field(e,4)},newTreeIndex:function(e){return r.platform.readInt32Field(e,8)},moveToSiblingIndex:function(e){return r.platform.readInt32Field(e,8)},removedAttributeName:function(e){return r.platform.readStringField(e,16)}},c={structLength:36,frameType:function(e){return r.platform.readInt16Field(e,4)},subtreeLength:function(e){return r.platform.readInt32Field(e,8)},elementReferenceCaptureId:function(e){return r.platform.readStringField(e,16)},componentId:function(e){return r.platform.readInt32Field(e,12)},elementName:function(e){return r.platform.readStringField(e,16)},textContent:function(e){return r.platform.readStringField(e,16)},markupContent:function(e){return r.platform.readStringField(e,16)},attributeName:function(e){return r.platform.readStringField(e,16)},attributeValue:function(e){return r.platform.readStringField(e,24)},attributeEventHandlerId:function(e){return r.platform.readUint64Field(e,8)}};function l(e,t,n){return r.platform.getArrayEntryPtr(e,t,n)}},function(e,t,n){"use strict";var r=this&&this.__awaiter||function(e,t,n,r){return new(n||(n=Promise))(function(o,a){function i(e){try{s(r.next(e))}catch(e){a(e)}}function u(e){try{s(r.throw(e))}catch(e){a(e)}}function s(e){e.done?o(e.value):new n(function(t){t(e.value)}).then(i,u)}s((r=r.apply(e,t||[])).next())})},o=this&&this.__generator||function(e,t){var n,r,o,a,i={label:0,sent:function(){if(1&o[0])throw o[1];return o[1]},trys:[],ops:[]};return a={next:u(0),throw:u(1),return:u(2)},"function"==typeof Symbol&&(a[Symbol.iterator]=function(){return this}),a;function u(a){return function(u){return function(a){if(n)throw new TypeError("Generator is already executing.");for(;i;)try{if(n=1,r&&(o=2&a[0]?r.return:a[0]?r.throw||((o=r.return)&&o.call(r),0):r.next)&&!(o=o.call(r,a[1])).done)return o;switch(r=0,o&&(a=[2&a[0],o.value]),a[0]){case 0:case 1:o=a;break;case 4:return i.label++,{value:a[1],done:!1};case 5:i.label++,r=a[1],a=[0];continue;case 7:a=i.ops.pop(),i.trys.pop();continue;default:if(!(o=(o=i.trys).length>0&&o[o.length-1])&&(6===a[0]||2===a[0])){i=0;continue}if(3===a[0]&&(!o||a[1]>o[0]&&a[1]0&&o[o.length-1])&&(6===a[0]||2===a[0])){i=0;continue}if(3===a[0]&&(!o||a[1]>o[0]&&a[1]0&&o[o.length-1])&&(6===a[0]||2===a[0])){i=0;continue}if(3===a[0]&&(!o||a[1]>o[0]&&a[1] 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_Object => { + const culturesToLoad = BINDING.mono_array_to_js_array(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()) + .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(() => { diff --git a/src/Components/Web.JS/src/Platform/Mono/MonoTypes.ts b/src/Components/Web.JS/src/Platform/Mono/MonoTypes.ts index f3d3e9fb7942..9b23de23f843 100644 --- a/src/Components/Web.JS/src/Platform/Mono/MonoTypes.ts +++ b/src/Components/Web.JS/src/Platform/Mono/MonoTypes.ts @@ -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; + mono_obj_array_set(array: System_Array, 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(array: Array): Pointer; + js_typed_array_to_array(array: Uint8Array): System_Object; + js_to_mono_obj(jsObject: any) : System_Object; + mono_array_to_js_array(array: System_Array) : Array; conv_string(dotnetString: System_String | null): string | null; bind_static_method(fqn: string, signature?: string): Function; } diff --git a/src/Components/Web.JS/src/Platform/WebAssemblyConfigLoader.ts b/src/Components/Web.JS/src/Platform/WebAssemblyConfigLoader.ts index c95fc0b00cb3..646157383a21 100644 --- a/src/Components/Web.JS/src/Platform/WebAssemblyConfigLoader.ts +++ b/src/Components/Web.JS/src/Platform/WebAssemblyConfigLoader.ts @@ -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 { @@ -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; diff --git a/src/Components/WebAssembly/Build/src/Tasks/GenerateBlazorBootJson.cs b/src/Components/WebAssembly/Build/src/Tasks/GenerateBlazorBootJson.cs index f323fee5e57a..a99113e0d787 100644 --- a/src/Components/WebAssembly/Build/src/Tasks/GenerateBlazorBootJson.cs +++ b/src/Components/WebAssembly/Build/src/Tasks/GenerateBlazorBootJson.cs @@ -5,6 +5,7 @@ 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 @@ internal void WriteBootJson(Stream output, string entryAssemblyName) cacheBootResources = CacheBootResources, debugBuild = DebugBuild, linkerEnabled = LinkerEnabled, - resources = new Dictionary(), + resources = new ResourcesData(), config = new List(), }; // 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(resourceTypeMetadata, out var resourceType)) + ResourceHashesByNameDictionary resourceList; + switch (resourceTypeMetadata) { - throw new NotSupportedException($"Unsupported BootManifestResourceType metadata value: {resourceTypeMetadata}"); - } - - if (!result.resources.TryGetValue(resourceType, out var resourceList)) - { - resourceList = new ResourceHashesByNameDictionary(); - result.resources.Add(resourceType, resourceList); + 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(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 (!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 @@ 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 Dictionary resources { get; set; } + public ResourcesData resources { get; set; } = new ResourcesData(); /// /// Gets a value that determines whether to enable caching of the @@ -166,11 +195,29 @@ public class BootJsonData public List config { get; set; } } - public enum ResourceType + public class ResourcesData { - assembly, - pdb, - runtime, + /// + /// .NET Wasm runtime resources (dotnet.wasm, dotnet.js) etc. + /// + public ResourceHashesByNameDictionary runtime { get; set; } = new ResourceHashesByNameDictionary(); + + /// + /// "assembly" (.dll) resources + /// + public ResourceHashesByNameDictionary assembly { get; set; } = new ResourceHashesByNameDictionary(); + + /// + /// "debug" (.pdb) resources + /// + [DataMember(EmitDefaultValue = false)] + public ResourceHashesByNameDictionary pdb { get; set; } + + /// + /// localization (.satellite resx) resources + /// + [DataMember(EmitDefaultValue = false)] + public Dictionary satelliteResources { get; set; } } #pragma warning restore IDE1006 // Naming Styles } diff --git a/src/Components/WebAssembly/Build/src/targets/Blazor.MonoRuntime.props b/src/Components/WebAssembly/Build/src/targets/Blazor.MonoRuntime.props index 3722992592e8..c25b1a02b61e 100644 --- a/src/Components/WebAssembly/Build/src/targets/Blazor.MonoRuntime.props +++ b/src/Components/WebAssembly/Build/src/targets/Blazor.MonoRuntime.props @@ -5,8 +5,7 @@ --disable-opt unreachablebodies --verbose --strip-security true --exclude-feature com -v false -c link -u link -b true <_BlazorJsPath Condition="'$(_BlazorJsPath)' == ''">$(MSBuildThisFileDirectory)..\tools\blazor\blazor.webassembly.js - <_BaseBlazorDistPath>dist\ - <_BaseBlazorRuntimeOutputPath>$(_BaseBlazorDistPath)_framework\ + <_BaseBlazorRuntimeOutputPath>_framework\ <_BlazorRuntimeBinOutputPath>$(_BaseBlazorRuntimeOutputPath)_bin\ <_BlazorRuntimeWasmOutputPath>$(_BaseBlazorRuntimeOutputPath)wasm\ <_BlazorBuiltInBclLinkerDescriptor>$(MSBuildThisFileDirectory)BuiltInBclLinkerDescriptor.xml diff --git a/src/Components/WebAssembly/Build/src/targets/Blazor.MonoRuntime.targets b/src/Components/WebAssembly/Build/src/targets/Blazor.MonoRuntime.targets index 7f399244700d..36bf22b72965 100644 --- a/src/Components/WebAssembly/Build/src/targets/Blazor.MonoRuntime.targets +++ b/src/Components/WebAssembly/Build/src/targets/Blazor.MonoRuntime.targets @@ -50,14 +50,14 @@ --> - <_BlazorManagedRuntimeAssemby Include="@(RuntimeCopyLocalItems)" /> + <_BlazorManagedRuntimeAssembly Include="@(RuntimeCopyLocalItems)" /> <_BlazorUserRuntimeAssembly Include="@(ReferencePath->WithMetadataValue('CopyLocal', 'true'))" /> <_BlazorUserRuntimeAssembly Include="@(ReferenceDependencyPaths->WithMetadataValue('CopyLocal', 'true'))" /> - <_BlazorManagedRuntimeAssemby Include="@(_BlazorUserRuntimeAssembly)" /> - <_BlazorManagedRuntimeAssemby Include="@(IntermediateAssembly)" /> + <_BlazorManagedRuntimeAssembly Include="@(_BlazorUserRuntimeAssembly)" /> + <_BlazorManagedRuntimeAssembly Include="@(IntermediateAssembly)" /> @@ -70,73 +70,68 @@ satellite assemblies, this should include all assemblies needed to run the application. --> + <_BlazorJSFile Include="$(_BlazorJSPath)" /> + <_BlazorJSFile Include="$(_BlazorJSMapPath)" Condition="Exists('$(_BlazorJSMapPath)')" /> + <_DotNetWasmRuntimeFile Include="$(ComponentsWebAssemblyRuntimePath)*" /> + - <_BlazorCopyLocalPaths Include="@(ReferenceCopyLocalPaths)" Condition="'%(Extension)' == '.dll'" /> - <_BlazorCopyLocalPaths Remove="@(_BlazorManagedRuntimeAssemby)" /> + <_BlazorCopyLocalPaths Include="@(ReferenceCopyLocalPaths)" + Exclude="@(_BlazorManagedRuntimeAssembly);@(ReferenceSatellitePaths)" + Condition="'%(Extension)' == '.dll'" /> + + <_BlazorCopyLocalPaths Include="@(IntermediateSatelliteAssembliesWithTargetPath)"> + %(IntermediateSatelliteAssembliesWithTargetPath.Culture)\ + <_BlazorOutputWithTargetPath Include="@(_BlazorCopyLocalPaths)"> - assembly - pdb + assembly + satellite %(_BlazorCopyLocalPaths.DestinationSubDirectory)%(FileName)%(Extension) $(_BlazorRuntimeBinOutputPath)%(_BlazorCopyLocalPaths.DestinationSubDirectory)%(FileName)%(Extension) - <_BlazorOutputWithTargetPath Include="@(_BlazorResolvedAssembly)"> - assembly - pdb - %(FileName)%(Extension) - $(_BlazorRuntimeBinOutputPath)%(FileName)%(Extension) + <_BlazorOutputWithTargetPath Include="@(ReferenceSatellitePaths)"> + $([System.String]::Copy('%(ReferenceSatellitePaths.DestinationSubDirectory)').Trim('\').Trim('/')) + satellite + %(ReferenceSatellitePaths.DestinationSubDirectory)%(FileName)%(Extension) + $(_BlazorRuntimeBinOutputPath)%(ReferenceSatellitePaths.DestinationSubDirectory)%(FileName)%(Extension) - - - - <_BlazorOutputWithTargetPath Remove="@(_BlazorOutputWithTargetPath)" Condition="'%(Extension)' == '.pdb'" /> - - - - - - <_BlazorCopyLocalPaths Include="@(ReferenceCopyLocalPaths)" Condition="'%(Extension)' == '.dll'" /> - <_BlazorCopyLocalPaths Remove="@(_BlazorManagedRuntimeAssemby)" Condition="'%(Extension)' == '.dll'" /> - - <_BlazorOutputWithTargetPath Include="@(_BlazorCopyLocalPaths)"> + <_BlazorOutputWithTargetPath Include="@(_BlazorResolvedAssembly)"> assembly - pdb + pdb %(_BlazorCopyLocalPaths.DestinationSubDirectory)%(FileName)%(Extension) $(_BlazorRuntimeBinOutputPath)%(_BlazorCopyLocalPaths.DestinationSubDirectory)%(FileName)%(Extension) - - - <_DotNetWasmRuntimeFile Include="$(ComponentsWebAssemblyRuntimePath)*" /> <_BlazorOutputWithTargetPath Include="@(_DotNetWasmRuntimeFile)"> $(_BlazorRuntimeWasmOutputPath)%(FileName)%(Extension) runtime %(FileName)%(Extension) - <_BlazorJSFile Include="$(_BlazorJSPath)" /> - <_BlazorJSFile Include="$(_BlazorJSMapPath)" Condition="Exists('$(_BlazorJSMapPath)')" /> <_BlazorOutputWithTargetPath Include="@(_BlazorJSFile)"> $(_BaseBlazorRuntimeOutputPath)%(FileName)%(Extension) + + + + <_BlazorOutputWithTargetPath Remove="@(_BlazorOutputWithTargetPath)" Condition="'%(Extension)' == '.pdb'" /> + diff --git a/src/Components/WebAssembly/Build/src/targets/StaticWebAssets.targets b/src/Components/WebAssembly/Build/src/targets/StaticWebAssets.targets index 0596817fac39..f0794babba18 100644 --- a/src/Components/WebAssembly/Build/src/targets/StaticWebAssets.targets +++ b/src/Components/WebAssembly/Build/src/targets/StaticWebAssets.targets @@ -7,7 +7,7 @@ $(PackageId) $([MSBuild]::NormalizeDirectory('$(TargetDir)wwwroot\')) $(StaticWebAssetBasePath) - $([System.String]::Copy('%(_BlazorOutputWithTargetPath.TargetOutputPath)').Replace('\','/').Replace('dist/','')) + $([System.String]::Copy('%(_BlazorOutputWithTargetPath.TargetOutputPath)').Replace('\','/')) diff --git a/src/Components/WebAssembly/Build/test/BuildIntegrationTests/BuildIntegrationTest.cs b/src/Components/WebAssembly/Build/test/BuildIntegrationTests/BuildIntegrationTest.cs index 0f87f1e63c11..bfbb4b2afdba 100644 --- a/src/Components/WebAssembly/Build/test/BuildIntegrationTests/BuildIntegrationTest.cs +++ b/src/Components/WebAssembly/Build/test/BuildIntegrationTests/BuildIntegrationTest.cs @@ -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 @@ public async Task Build_WithBlazorWebAssemblyEnableLinkingFalse_SatelliteAssembl 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( + 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); } } } diff --git a/src/Components/WebAssembly/Build/test/GenerateBlazorBootJsonTest.cs b/src/Components/WebAssembly/Build/test/GenerateBlazorBootJsonTest.cs index 1429030b303f..552003707728 100644 --- a/src/Components/WebAssembly/Build/test/GenerateBlazorBootJsonTest.cs +++ b/src/Components/WebAssembly/Build/test/GenerateBlazorBootJsonTest.cs @@ -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 @@ public void GroupsResourcesByType() 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 @@ public void GroupsResourcesByType() // Assert var parsedContent = ParseBootData(stream); Assert.Equal("MyEntrypointAssembly", parsedContent.entryAssembly); - Assert.Collection(parsedContent.resources.Keys, - resourceListKey => - { - 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 - }, - 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.pdb, resourceListKey); - Assert.Single(resources); - Assert.Equal("sha256-pdbhashpdbhashpdbhash", resources["otherdir/SomePdb.pdb"]); + 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.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 @@ private static BootJsonData ParseBootData(Stream stream) 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(); - 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; } } diff --git a/src/Components/WebAssembly/Build/testassets/classlibrarywithsatelliteassemblies/Resources.es-ES.resx b/src/Components/WebAssembly/Build/testassets/classlibrarywithsatelliteassemblies/Resources.es-ES.resx new file mode 100644 index 000000000000..0ab8f0ddfb32 --- /dev/null +++ b/src/Components/WebAssembly/Build/testassets/classlibrarywithsatelliteassemblies/Resources.es-ES.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Hola + + \ No newline at end of file diff --git a/src/Components/WebAssembly/Build/testassets/standalone/Resources.ja.resx b/src/Components/WebAssembly/Build/testassets/standalone/Resources.ja.resx new file mode 100644 index 000000000000..cd5b68cfc57a --- /dev/null +++ b/src/Components/WebAssembly/Build/testassets/standalone/Resources.ja.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Konnichiwa + + \ No newline at end of file diff --git a/src/Components/WebAssembly/WebAssembly/src/Hosting/EntrypointInvoker.cs b/src/Components/WebAssembly/WebAssembly/src/Hosting/EntrypointInvoker.cs index 4fc09aaa0133..1fe153f17a17 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Hosting/EntrypointInvoker.cs +++ b/src/Components/WebAssembly/WebAssembly/src/Hosting/EntrypointInvoker.cs @@ -16,33 +16,35 @@ internal static class EntrypointInvoker // 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() } : 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) diff --git a/src/Components/WebAssembly/WebAssembly/src/Hosting/SatelliteResourcesLoader.cs b/src/Components/WebAssembly/WebAssembly/src/Hosting/SatelliteResourcesLoader.cs new file mode 100644 index 000000000000..c4a5c33c689e --- /dev/null +++ b/src/Components/WebAssembly/WebAssembly/src/Hosting/SatelliteResourcesLoader.cs @@ -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 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>( + GetSatelliteAssemblies, + culturesToLoad.ToArray(), + null, + null); + + if (count == 0) + { + return; + } + + // readSatelliteAssemblies resolves the assembly bytes + var assemblies = _invoker.InvokeUnmarshalled( + ReadSatelliteAssemblies, + null, + null, + null); + + for (var i = 0; i < assemblies.Length; i++) + { + Assembly.Load((byte[])assemblies[i]); + } + } + + internal static List GetCultures(CultureInfo cultureInfo) + { + var culturesToLoad = new List(); + + // 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; + } + } +} diff --git a/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHost.cs b/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHost.cs index 3cad56ac8a84..4b3114b50647 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHost.cs +++ b/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHost.cs @@ -58,6 +58,8 @@ internal WebAssemblyHost(IServiceProvider services, IServiceScope scope, IConfig /// public IServiceProvider Services => _scope.ServiceProvider; + internal SatelliteResourcesLoader SatelliteResourcesLoader { get; set; } = SatelliteResourcesLoader.Instance; + /// /// Disposes the host asynchronously. /// @@ -132,6 +134,10 @@ internal async Task RunAsyncCore(CancellationToken cancellationToken) 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; } } diff --git a/src/Components/WebAssembly/WebAssembly/src/Properties/AssemblyInfo.cs b/src/Components/WebAssembly/WebAssembly/src/Properties/AssemblyInfo.cs new file mode 100644 index 000000000000..47bfb5013ffc --- /dev/null +++ b/src/Components/WebAssembly/WebAssembly/src/Properties/AssemblyInfo.cs @@ -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")] diff --git a/src/Components/WebAssembly/WebAssembly/test/Hosting/EntrypointInvokerTest.cs b/src/Components/WebAssembly/WebAssembly/test/Hosting/EntrypointInvokerTest.cs index eafc042993d4..aef12b3c7f4c 100644 --- a/src/Components/WebAssembly/WebAssembly/test/Hosting/EntrypointInvokerTest.cs +++ b/src/Components/WebAssembly/WebAssembly/test/Hosting/EntrypointInvokerTest.cs @@ -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 @@ public void InvokesEntrypoint_Sync_Success(bool hasReturnValue, bool hasParams) }", 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 @@ public void InvokesEntrypoint_Async_Success(bool hasReturnValue, bool hasParams) // 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 @@ public static class Program return assembly; } + + private class TestSatelliteResourcesLoader : SatelliteResourcesLoader + { + internal TestSatelliteResourcesLoader() + : base(WebAssemblyJSRuntimeInvoker.Instance) + { + } + + protected override ValueTask LoadSatelliteAssembliesForCurrentCultureAsync() => default; + } } } diff --git a/src/Components/WebAssembly/WebAssembly/test/Hosting/SatelliteResourcesLoaderTest.cs b/src/Components/WebAssembly/WebAssembly/test/Hosting/SatelliteResourcesLoaderTest.cs new file mode 100644 index 000000000000..d541d404dc3c --- /dev/null +++ b/src/Components/WebAssembly/WebAssembly/test/Hosting/SatelliteResourcesLoaderTest.cs @@ -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(); + invoker.Setup(i => i.InvokeUnmarshalled>(GetSatelliteAssemblies, new[] { "en-GB", "en" }, null, null)) + .Returns(Task.FromResult(1)) + .Verifiable(); + + invoker.Setup(i => i.InvokeUnmarshalled(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(); + invoker.Setup(i => i.InvokeUnmarshalled>(GetSatelliteAssemblies, new[] { "en-GB", "en" }, null, null)) + .Returns(Task.FromResult(0)) + .Verifiable(); + + var loader = new SatelliteResourcesLoader(invoker.Object); + + // Act + await loader.LoadCurrentCultureResourcesAsync(); + + // Assert + invoker.Verify(i => i.InvokeUnmarshalled(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(); + invoker.Setup(i => i.InvokeUnmarshalled>(GetSatelliteAssemblies, It.IsAny(), null, null)) + .Returns(Task.FromResult(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>(GetSatelliteAssemblies, It.IsAny(), 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(); + invoker.Setup(i => i.InvokeUnmarshalled>(GetSatelliteAssemblies, It.IsAny(), null, null)) + .Returns(Task.FromResult(0));; + + var loader = new SatelliteResourcesLoader(invoker.Object); + + // Act + await loader.LoadCurrentCultureResourcesAsync(); + await loader.LoadCurrentCultureResourcesAsync(); + + invoker.Verify(i => i.InvokeUnmarshalled>(GetSatelliteAssemblies, It.IsAny(), null, null), + Times.Once()); + } + } +} diff --git a/src/Components/WebAssembly/WebAssembly/test/Hosting/WebAssemblyHostTest.cs b/src/Components/WebAssembly/WebAssembly/test/Hosting/WebAssemblyHostTest.cs index 14a404e68676..c82725013491 100644 --- a/src/Components/WebAssembly/WebAssembly/test/Hosting/WebAssemblyHostTest.cs +++ b/src/Components/WebAssembly/WebAssembly/test/Hosting/WebAssemblyHostTest.cs @@ -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 @@ public async Task RunAsync_CanExitBasedOnCancellationToken() // Arrange var builder = new WebAssemblyHostBuilder(new TestWebAssemblyJSRuntimeInvoker()); var host = builder.Build(); + host.SatelliteResourcesLoader = new TestSatelliteResourcesLoader(); var cts = new CancellationTokenSource(); @@ -38,6 +40,7 @@ public async Task RunAsync_CallingTwiceCausesException() // 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 @@ public async Task DisposeAsync_CanDisposeAfterCallingRunAsync() var builder = new WebAssemblyHostBuilder(new TestWebAssemblyJSRuntimeInvoker()); builder.Services.AddSingleton(); var host = builder.Build(); + host.SatelliteResourcesLoader = new TestSatelliteResourcesLoader(); var disposable = host.Services.GetRequiredService(); @@ -87,5 +91,15 @@ public ValueTask DisposeAsync() return new ValueTask(Task.CompletedTask); } } + + private class TestSatelliteResourcesLoader : SatelliteResourcesLoader + { + internal TestSatelliteResourcesLoader() + : base(WebAssemblyJSRuntimeInvoker.Instance) + { + } + + protected override ValueTask LoadSatelliteAssembliesForCurrentCultureAsync() => default; + } } } diff --git a/src/Components/test/E2ETest/ServerExecutionTests/ServerGlobalizationTest.cs b/src/Components/test/E2ETest/ServerExecutionTests/ServerGlobalizationTest.cs new file mode 100644 index 000000000000..97cbe44b59b9 --- /dev/null +++ b/src/Components/test/E2ETest/ServerExecutionTests/ServerGlobalizationTest.cs @@ -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> + { + public ServerGlobalizationTest( + BrowserFixture browserFixture, + BasicTestAppServerSiteFixture serverFixture, + ITestOutputHelper output) + : base(browserFixture, serverFixture, output) + { + } + + protected override TimeSpan UtcOffset => TimeZoneInfo.Local.BaseUtcOffset; + + protected override void InitializeAsyncCore() + { + Navigate(ServerPathBase); + Browser.MountTestComponent(); + 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(); + Browser.Exists(By.Id("globalization-cases")); + + var cultureDisplay = Browser.Exists(By.Id("culture-name-display")); + Assert.Equal($"Culture is: {culture}", cultureDisplay.Text); + } + } +} diff --git a/src/Components/test/E2ETest/ServerExecutionTests/LocalizationTest.cs b/src/Components/test/E2ETest/ServerExecutionTests/ServerLocalizationTest.cs similarity index 87% rename from src/Components/test/E2ETest/ServerExecutionTests/LocalizationTest.cs rename to src/Components/test/E2ETest/ServerExecutionTests/ServerLocalizationTest.cs index 49d31717fccf..98aabb2676b0 100644 --- a/src/Components/test/E2ETest/ServerExecutionTests/LocalizationTest.cs +++ b/src/Components/test/E2ETest/ServerExecutionTests/ServerLocalizationTest.cs @@ -13,11 +13,9 @@ 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> + public class ServerLocalizationTest : ServerTestBase> { - public LocalizationTest( + public ServerLocalizationTest( BrowserFixture browserFixture, BasicTestAppServerSiteFixture serverFixture, ITestOutputHelper output) diff --git a/src/Components/test/E2ETest/ServerExecutionTests/GlobalizationTest.cs b/src/Components/test/E2ETest/Tests/GlobalizationTest.cs similarity index 82% rename from src/Components/test/E2ETest/ServerExecutionTests/GlobalizationTest.cs rename to src/Components/test/E2ETest/Tests/GlobalizationTest.cs index 76a01c13f3b3..7870a3049e8c 100644 --- a/src/Components/test/E2ETest/ServerExecutionTests/GlobalizationTest.cs +++ b/src/Components/test/E2ETest/Tests/GlobalizationTest.cs @@ -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> + public abstract class GlobalizationTest : ServerTestBase + where TServerFixture : ServerFixture { - public GlobalizationTest( - BrowserFixture browserFixture, - BasicTestAppServerSiteFixture serverFixture, - ITestOutputHelper output) + public GlobalizationTest(BrowserFixture browserFixture, TServerFixture serverFixture, ITestOutputHelper output) : base(browserFixture, serverFixture, output) { } - protected override void InitializeAsyncCore() - { - Navigate(ServerPathBase); - Browser.MountTestComponent(); - Browser.Exists(By.Id("culture-selector")); - } + protected abstract void SetCulture(string culture); + + /// + /// 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 to express this difference in calculating expected values in these tests. + /// + protected abstract TimeSpan UtcOffset { get; } [Theory] [InlineData("en-US")] @@ -74,11 +69,11 @@ public void CanSetCultureAndParseCultueSensitiveNumbersAndDates(string culture) // 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 @@ public void CanSetCultureAndParseCultureInvariantNumbersAndDatesWithInputFields( 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 @@ public void CanSetCultureAndParseCultureInvariantNumbersAndDatesWithFormComponen 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(); - 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")); } } } diff --git a/src/Components/test/E2ETest/Tests/WebAssemblyGlobalizationTest.cs b/src/Components/test/E2ETest/Tests/WebAssemblyGlobalizationTest.cs new file mode 100644 index 000000000000..58bda0cf8dcc --- /dev/null +++ b/src/Components/test/E2ETest/Tests/WebAssemblyGlobalizationTest.cs @@ -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> + { + public WebAssemblyGlobalizationTest( + BrowserFixture browserFixture, + ToggleExecutionModeServerFixture 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(); + Browser.Exists(By.Id("globalization-cases")); + + var cultureDisplay = Browser.Exists(By.Id("culture-name-display")); + Assert.Equal($"Culture is: {culture}", cultureDisplay.Text); + } + } +} diff --git a/src/Components/test/E2ETest/Tests/WebAssemblyLocalizationTest.cs b/src/Components/test/E2ETest/Tests/WebAssemblyLocalizationTest.cs new file mode 100644 index 000000000000..81c99ccf677a --- /dev/null +++ b/src/Components/test/E2ETest/Tests/WebAssemblyLocalizationTest.cs @@ -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> + { + public WebAssemblyLocalizationTest( + BrowserFixture browserFixture, + ToggleExecutionModeServerFixture 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(); + + 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); + } + } +} diff --git a/src/Components/test/testassets/BasicTestApp/Program.cs b/src/Components/test/testassets/BasicTestApp/Program.cs index 346702006dc8..d0b7dadd1e93 100644 --- a/src/Components/test/testassets/BasicTestApp/Program.cs +++ b/src/Components/test/testassets/BasicTestApp/Program.cs @@ -7,7 +7,9 @@ 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 @@ public static async Task Main(string[] args) { 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 @@ public static async Task Main(string[] args) 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().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