Blazor WebAssembly caching fixes (#19235)

* Support logging errors that happen really early

* Tolerate all the ways caching might be unavailable

* Include dotnet.js in blazor.boot.json

* Reorganize boot manifest to categorize files by role, not just by filename extension

* Enable cache-busting and SRI check on dotnet.js

* Change cache-busting to vary filename, not using querystring. Needed to make PWA manifest still work.
This commit is contained in:
Steve Sanderson 2020-02-21 17:35:36 +00:00 committed by GitHub
parent c9c06f573d
commit 6fe946e633
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 143 additions and 74 deletions

File diff suppressed because one or more lines are too long

View File

@ -52,6 +52,12 @@ async function boot(options?: any): Promise<void> {
window['Blazor'].start = boot;
if (shouldAutoStart()) {
boot().catch(error => {
Module.printErr(error); // Logs it, and causes the error UI to appear
if (typeof Module !== 'undefined' && Module.printErr) {
// Logs it, and causes the error UI to appear
Module.printErr(error);
} else {
// The error must have happened so early we didn't yet set up the error UI, so just log to console
console.error(error);
}
});
}

View File

@ -24,7 +24,7 @@ export const monoPlatform: Platform = {
// before we start loading the WebAssembly files
addGlobalModuleScriptTagsToDocument(() => {
window['Module'] = createEmscriptenModuleInstance(resourceLoader, resolve, reject);
addScriptTagsToDocument();
addScriptTagsToDocument(resourceLoader);
});
});
},
@ -112,15 +112,29 @@ export const monoPlatform: Platform = {
},
};
function addScriptTagsToDocument() {
function addScriptTagsToDocument(resourceLoader: WebAssemblyResourceLoader) {
const browserSupportsNativeWebAssembly = typeof WebAssembly !== 'undefined' && WebAssembly.validate;
if (!browserSupportsNativeWebAssembly) {
throw new Error('This browser does not support WebAssembly.');
}
// The dotnet.*.js file has a version or hash in its name as a form of cache-busting. This is needed
// because it's the only part of the loading process that can't use cache:'no-cache' (because it's
// not a 'fetch') and isn't controllable by the developer (so they can't put in their own cache-busting
// querystring). So, to find out the exact URL we have to search the boot manifest.
const dotnetJsResourceName = Object
.keys(resourceLoader.bootConfig.resources.runtime)
.filter(n => n.startsWith('dotnet.') && n.endsWith('.js'))[0];
const scriptElem = document.createElement('script');
scriptElem.src = '_framework/wasm/dotnet.js';
scriptElem.src = `_framework/wasm/${dotnetJsResourceName}`;
scriptElem.defer = true;
// For consistency with WebAssemblyResourceLoader, we only enforce SRI if caching is allowed
if (resourceLoader.bootConfig.cacheBootResources) {
const contentHash = resourceLoader.bootConfig.resources.runtime[dotnetJsResourceName];
scriptElem.integrity = contentHash;
}
document.body.appendChild(scriptElem);
}
@ -165,8 +179,8 @@ function createEmscriptenModuleInstance(resourceLoader: WebAssemblyResourceLoade
const dotnetWasmResource = await resourceLoader.loadResource(
/* name */ dotnetWasmResourceName,
/* url */ `_framework/wasm/${dotnetWasmResourceName}`,
/* hash */ resourceLoader.bootConfig.resources.wasm[dotnetWasmResourceName]);
compiledInstance = await compileWasmModule(dotnetWasmResource, imports);
/* hash */ resourceLoader.bootConfig.resources.runtime[dotnetWasmResourceName]);
compiledInstance = await compileWasmModule(dotnetWasmResource, imports);
} catch (ex) {
module.printErr(ex);
throw ex;

View File

@ -12,19 +12,13 @@ export class WebAssemblyResourceLoader {
credentials: 'include',
cache: networkFetchCacheMode
});
const bootConfig: BootJsonData = await bootConfigResponse.json();
const cache = await getCacheToUseIfEnabled(bootConfig);
// Define a separate cache for each base href, so we're isolated from any other
// Blazor application running on the same origin. We need this so that we're free
// to purge from the cache anything we're not using and don't let it keep growing,
// since we don't want to be worst offenders for space usage.
const relativeBaseHref = document.baseURI.substring(document.location.origin.length);
const cacheName = `blazor-resources-${relativeBaseHref}`;
return new WebAssemblyResourceLoader(
await bootConfigResponse.json(),
await caches.open(cacheName));
return new WebAssemblyResourceLoader(bootConfig, cache);
}
constructor (public readonly bootConfig: BootJsonData, private cache: Cache)
constructor (public readonly bootConfig: BootJsonData, private cacheIfUsed: Cache | null)
{
}
@ -34,21 +28,15 @@ export class WebAssemblyResourceLoader {
}
loadResource(name: string, url: string, contentHash: string): LoadingResource {
// Setting 'cacheBootResources' to false bypasses the entire cache flow, including integrity checking.
// This gives developers an easy opt-out if they don't like anything about the default cache mechanism.
// Note that if cacheBootResources was explicitly disabled, we also bypass hash checking
// This is to give developers an easy opt-out from the entire caching/validation flow if
// there's anything they don't like about it.
// There's also a Chromium bug we need to work around here: the CacheStorage APIs say that when
// caches.open(name) returns a promise that succeeds, the value is meant to be a Cache instance.
// However, if the browser was launched with a --user-data-dir param that's "too long" in some sense,
// then even through the promise resolves as success, the value given is `undefined`.
// See https://stackoverflow.com/a/46626574. We're reporting this to Chromium and others, but in
// the meantime, if this.cache isn't set, just proceed without caching.
const useCache = this.bootConfig.cacheBootResources && this.cache;
const response = this.cacheIfUsed
? this.loadResourceWithCaching(this.cacheIfUsed, name, url, contentHash)
: fetch(url, { cache: networkFetchCacheMode, integrity: this.bootConfig.cacheBootResources ? contentHash : undefined });
const response = useCache
? this.loadResourceWithCaching(name, url, contentHash)
: fetch(url, { cache: networkFetchCacheMode });
return { name, url, response };
return { name, url, response };
}
logToConsole() {
@ -57,8 +45,12 @@ export class WebAssemblyResourceLoader {
const cacheResponseBytes = countTotalBytes(cacheLoadsEntries);
const networkResponseBytes = countTotalBytes(networkLoadsEntries);
const totalResponseBytes = cacheResponseBytes + networkResponseBytes;
const linkerDisabledWarning = this.bootConfig.linkerEnabled ? '%c' : '\n%cThis application was built with linking (tree shaking) disabled. Published applications will be significantly smaller.';
if (totalResponseBytes === 0) {
// We have no perf stats to display, likely because caching is not in use.
return;
}
const linkerDisabledWarning = this.bootConfig.linkerEnabled ? '%c' : '\n%cThis application was built with linking (tree shaking) disabled. Published applications will be significantly smaller.';
console.groupCollapsed(`%cblazor%c Loaded ${toDataSizeString(totalResponseBytes)} resources${linkerDisabledWarning}`, 'background: purple; color: white; padding: 1px 3px; border-radius: 3px;', 'font-weight: bold;', 'font-weight: normal;');
if (cacheLoadsEntries.length) {
@ -79,17 +71,20 @@ export class WebAssemblyResourceLoader {
async purgeUnusedCacheEntriesAsync() {
// We want to keep the cache small because, even though the browser will evict entries if it
// gets too big, we don't want to be considered problematic by the end user viewing storage stats
const cachedRequests = await this.cache.keys();
const deletionPromises = cachedRequests.map(async cachedRequest => {
if (!(cachedRequest.url in this.usedCacheKeys)) {
await this.cache.delete(cachedRequest);
}
});
const cache = this.cacheIfUsed;
if (cache) {
const cachedRequests = await cache.keys();
const deletionPromises = cachedRequests.map(async cachedRequest => {
if (!(cachedRequest.url in this.usedCacheKeys)) {
await cache.delete(cachedRequest);
}
});
return Promise.all(deletionPromises);
await Promise.all(deletionPromises);
}
}
private async loadResourceWithCaching(name: string, url: string, contentHash: string) {
private async loadResourceWithCaching(cache: Cache, name: string, url: string, contentHash: string) {
// Since we are going to cache the response, we require there to be a content hash for integrity
// checking. We don't want to cache bad responses. There should always be a hash, because the build
// process generates this data.
@ -100,7 +95,7 @@ export class WebAssemblyResourceLoader {
const cacheKey = toAbsoluteUri(`${url}.${contentHash}`);
this.usedCacheKeys[cacheKey] = true;
const cachedResponse = await this.cache.match(cacheKey);
const cachedResponse = await cache.match(cacheKey);
if (cachedResponse) {
// It's in the cache.
const responseBytes = parseInt(cachedResponse.headers.get('content-length') || '0');
@ -109,12 +104,12 @@ export class WebAssemblyResourceLoader {
} else {
// It's not in the cache. Fetch from network.
const networkResponse = await fetch(url, { cache: networkFetchCacheMode, integrity: contentHash });
this.addToCacheAsync(name, cacheKey, networkResponse); // Don't await - add to cache in background
this.addToCacheAsync(cache, name, cacheKey, networkResponse); // Don't await - add to cache in background
return networkResponse;
}
}
private async addToCacheAsync(name: string, cacheKey: string, response: Response) {
private async addToCacheAsync(cache: Cache, name: string, cacheKey: string, response: Response) {
// We have to clone in order to put this in the cache *and* not prevent other code from
// reading the original response stream.
const responseData = await response.clone().arrayBuffer();
@ -129,7 +124,7 @@ export class WebAssemblyResourceLoader {
// Add to cache as a custom response object so we can track extra data such as responseBytes
// We can't rely on the server sending content-length (ASP.NET Core doesn't by default)
await this.cache.put(cacheKey, new Response(responseData, {
await cache.put(cacheKey, new Response(responseData, {
headers: {
'content-type': response.headers.get('content-type') || '',
'content-length': (responseBytes || response.headers.get('content-length') || '').toString()
@ -138,6 +133,34 @@ export class WebAssemblyResourceLoader {
}
}
async function getCacheToUseIfEnabled(bootConfig: BootJsonData): Promise<Cache | null> {
// caches will be undefined if we're running on an insecure origin (secure means https or localhost)
if (!bootConfig.cacheBootResources || typeof caches === 'undefined') {
return null;
}
// Define a separate cache for each base href, so we're isolated from any other
// Blazor application running on the same origin. We need this so that we're free
// to purge from the cache anything we're not using and don't let it keep growing,
// since we don't want to be worst offenders for space usage.
const relativeBaseHref = document.baseURI.substring(document.location.origin.length);
const cacheName = `blazor-resources-${relativeBaseHref}`;
try {
// There's a Chromium bug we need to be aware of here: the CacheStorage APIs say that when
// caches.open(name) returns a promise that succeeds, the value is meant to be a Cache instance.
// However, if the browser was launched with a --user-data-dir param that's "too long" in some sense,
// then even through the promise resolves as success, the value given is `undefined`.
// See https://stackoverflow.com/a/46626574 and https://bugs.chromium.org/p/chromium/issues/detail?id=1054541
// If we see this happening, return "null" to mean "proceed without caching".
return (await caches.open(cacheName)) || null;
} catch {
// There's no known scenario where we should get an exception here, but considering the
// Chromium bug above, let's tolerate it and treat as "proceed without caching".
return null;
}
}
function countTotalBytes(loads: LoadLogEntry[]) {
return loads.reduce((prev, item) => prev + (item.responseBytes || 0), 0);
}
@ -162,9 +185,9 @@ interface BootJsonData {
}
interface ResourceGroups {
readonly wasm: ResourceList;
readonly assembly: ResourceList;
readonly pdb?: ResourceList;
readonly runtime: ResourceList;
}
interface LoadLogEntry {

View File

@ -110,7 +110,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Build
{
// If RelativeOutputPath was not specified, we assume the item will be placed at the
// root of whatever directory is used for its resource type (e.g., assemblies go in _bin)
outputPath = Path.GetFileName(item.ItemSpec);
outputPath = Path.GetFileName(item.GetMetadata("TargetOutputPath"));
}
return outputPath.Replace('\\', '/');
@ -159,7 +159,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Build
{
assembly,
pdb,
wasm
runtime,
}
#pragma warning restore IDE1006 // Naming Styles
}

View File

@ -76,13 +76,15 @@
<_BlazorCopyLocalPaths Remove="@(_BlazorManagedRuntimeAssemby)" />
<BlazorOutputWithTargetPath Include="@(_BlazorCopyLocalPaths)">
<BlazorRuntimeFile>true</BlazorRuntimeFile>
<_BlazorBootManifestResourceType Condition="'%(Extension)' == '.dll'">assembly</_BlazorBootManifestResourceType>
<_BlazorBootManifestResourceType Condition="'%(Extension)' == '.pdb'">pdb</_BlazorBootManifestResourceType>
<TargetOutputPath>$(BlazorRuntimeBinOutputPath)%(_BlazorCopyLocalPaths.DestinationSubDirectory)%(FileName)%(Extension)</TargetOutputPath>
<RelativeOutputPath>%(_BlazorCopyLocalPaths.DestinationSubDirectory)%(FileName)%(Extension)</RelativeOutputPath>
</BlazorOutputWithTargetPath>
<BlazorOutputWithTargetPath Include="@(_BlazorResolvedAssembly)">
<BlazorRuntimeFile>true</BlazorRuntimeFile>
<_BlazorBootManifestResourceType Condition="'%(Extension)' == '.dll'">assembly</_BlazorBootManifestResourceType>
<_BlazorBootManifestResourceType Condition="'%(Extension)' == '.pdb'">pdb</_BlazorBootManifestResourceType>
<TargetOutputPath>$(BlazorRuntimeBinOutputPath)%(FileName)%(Extension)</TargetOutputPath>
<RelativeOutputPath>%(FileName)%(Extension)</RelativeOutputPath>
</BlazorOutputWithTargetPath>
@ -111,21 +113,41 @@
<_BlazorCopyLocalPaths Remove="@(_BlazorManagedRuntimeAssemby)" Condition="'%(Extension)' == '.dll'" />
<BlazorOutputWithTargetPath Include="@(_BlazorCopyLocalPaths)">
<BlazorRuntimeFile>true</BlazorRuntimeFile>
<_BlazorBootManifestResourceType Condition="'%(Extension)' == '.dll'">assembly</_BlazorBootManifestResourceType>
<_BlazorBootManifestResourceType Condition="'%(Extension)' == '.pdb'">pdb</_BlazorBootManifestResourceType>
<TargetOutputPath>$(BlazorRuntimeBinOutputPath)%(_BlazorCopyLocalPaths.DestinationSubDirectory)%(FileName)%(Extension)</TargetOutputPath>
<RelativeOutputPath>%(_BlazorCopyLocalPaths.DestinationSubDirectory)%(FileName)%(Extension)</RelativeOutputPath>
</BlazorOutputWithTargetPath>
</ItemGroup>
<ItemGroup>
<MonoWasmFile Include="$(ComponentsWebAssemblyRuntimePath)*" />
<BlazorJSFile Include="$(BlazorJSPath)" />
<BlazorJSFile Include="$(BlazorJSMapPath)" Condition="Exists('$(BlazorJSMapPath)')" />
<!--
TODO: Instead of dynamically switching the TargetOutputPath for dotnet.js here, actually
change the physical filename inside the M.A.C.W.Runtime package so it includes the package's
version (e.g., dotnet.3.2.0-preview3.12345.js). Then we can eliminate the following property
and recombine the two item groups MonoWasmFile and MonoJsFile below, simply putting all of
$(ComponentsWebAssemblyRuntimePath)* into BlazorOutputWithTargetPath using their physical names.
The actual value 3.2.0-preview2 is hardcoded here until we update M.A.C.W.Runtime to do this.
-->
<PropertyGroup>
<TemporaryDotNetJsFileVersion>3.2.0-preview2</TemporaryDotNetJsFileVersion>
</PropertyGroup>
<ItemGroup>
<MonoWasmFile Include="$(ComponentsWebAssemblyRuntimePath)*.wasm" />
<BlazorOutputWithTargetPath Include="@(MonoWasmFile)">
<TargetOutputPath>$(BlazorRuntimeWasmOutputPath)%(FileName)%(Extension)</TargetOutputPath>
<BlazorRuntimeFile>true</BlazorRuntimeFile>
<_BlazorBootManifestResourceType>runtime</_BlazorBootManifestResourceType>
</BlazorOutputWithTargetPath>
<MonoJsFile Include="$(ComponentsWebAssemblyRuntimePath)*.js" />
<BlazorOutputWithTargetPath Include="@(MonoJsFile)">
<TargetOutputPath>$(BlazorRuntimeWasmOutputPath)%(FileName).$(TemporaryDotNetJsFileVersion)%(Extension)</TargetOutputPath>
<_BlazorBootManifestResourceType>runtime</_BlazorBootManifestResourceType>
</BlazorOutputWithTargetPath>
<BlazorJSFile Include="$(BlazorJSPath)" />
<BlazorJSFile Include="$(BlazorJSMapPath)" Condition="Exists('$(BlazorJSMapPath)')" />
<BlazorOutputWithTargetPath Include="@(BlazorJSFile)">
<TargetOutputPath>$(BaseBlazorRuntimeOutputPath)%(FileName)%(Extension)</TargetOutputPath>
</BlazorOutputWithTargetPath>
@ -300,13 +322,12 @@
Inputs="$(MSBuildAllProjects);@(BlazorOutputWithTargetPath)"
Outputs="$(BlazorBootJsonIntermediateOutputPath)">
<ItemGroup>
<_BlazorBootResource Include="@(BlazorOutputWithTargetPath->WithMetadataValue('BlazorRuntimeFile', 'true'))" />
<_BlazorBootResource BootResourceType="assembly" Condition="'%(Extension)' == '.dll'" />
<_BlazorBootResource BootResourceType="pdb" Condition="'%(Extension)' == '.pdb'" />
<_BlazorBootResource BootResourceType="wasm" Condition="'%(Extension)' == '.wasm'" />
<_BlazorBootResource Include="@(BlazorOutputWithTargetPath->HasMetadata('_BlazorBootManifestResourceType'))">
<BootResourceType>%(BlazorOutputWithTargetPath._BlazorBootManifestResourceType)</BootResourceType>
</_BlazorBootResource>
</ItemGroup>
<GetFileHash Files="@(_BlazorBootResource->HasMetadata('BootResourceType'))" Algorithm="SHA256" HashEncoding="base64">
<GetFileHash Files="@(_BlazorBootResource)" Algorithm="SHA256" HashEncoding="base64">
<Output TaskParameter="Items" ItemName="_BlazorBootResourceWithHash" />
</GetFileHash>

View File

@ -23,7 +23,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Build
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "blazor.boot.json");
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "blazor.webassembly.js");
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "wasm", "dotnet.wasm");
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "wasm", "dotnet.js");
Assert.FileCountEquals(result, 1, Path.Combine(buildOutputDirectory, "wwwroot", "_framework", "wasm"), "dotnet.*.js");
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "_bin", "standalone.dll");
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "_bin", "Microsoft.Extensions.Logging.Abstractions.dll"); // Verify dependencies are part of the output.
@ -71,7 +71,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Build
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "blazor.boot.json");
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "blazor.webassembly.js");
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "wasm", "dotnet.wasm");
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "wasm", "dotnet.js");
Assert.FileCountEquals(result, 1, Path.Combine(buildOutputDirectory, "wwwroot", "_framework", "wasm"), "dotnet.*.js");
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "_bin", "standalone.dll");
Assert.FileExists(result, buildOutputDirectory, "wwwroot", "_framework", "_bin", "Microsoft.Extensions.Logging.Abstractions.dll"); // Verify dependencies are part of the output.
}

View File

@ -24,7 +24,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Build
Assert.FileExists(result, blazorPublishDirectory, "_framework", "blazor.boot.json");
Assert.FileExists(result, blazorPublishDirectory, "_framework", "blazor.webassembly.js");
Assert.FileExists(result, blazorPublishDirectory, "_framework", "wasm", "dotnet.wasm");
Assert.FileExists(result, blazorPublishDirectory, "_framework", "wasm", "dotnet.js");
Assert.FileCountEquals(result, 1, Path.Combine(blazorPublishDirectory, "_framework", "wasm"), "dotnet.*.js");
Assert.FileExists(result, blazorPublishDirectory, "_framework", "_bin", "standalone.dll");
Assert.FileExists(result, blazorPublishDirectory, "_framework", "_bin", "Microsoft.Extensions.Logging.Abstractions.dll"); // Verify dependencies are part of the output.
@ -64,7 +64,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Build
Assert.FileExists(result, blazorPublishDirectory, "_framework", "blazor.boot.json");
Assert.FileExists(result, blazorPublishDirectory, "_framework", "blazor.webassembly.js");
Assert.FileExists(result, blazorPublishDirectory, "_framework", "wasm", "dotnet.wasm");
Assert.FileExists(result, blazorPublishDirectory, "_framework", "wasm", "dotnet.js");
Assert.FileCountEquals(result, 1, Path.Combine(blazorPublishDirectory, "_framework", "wasm"), "dotnet.*.js");
Assert.FileExists(result, blazorPublishDirectory, "_framework", "_bin", "standalone.dll");
Assert.FileExists(result, blazorPublishDirectory, "_framework", "_bin", "Microsoft.Extensions.Logging.Abstractions.dll"); // Verify dependencies are part of the output.
@ -102,7 +102,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Build
Assert.FileExists(result, blazorPublishDirectory, "_framework", "blazor.boot.json");
Assert.FileExists(result, blazorPublishDirectory, "_framework", "blazor.webassembly.js");
Assert.FileExists(result, blazorPublishDirectory, "_framework", "wasm", "dotnet.wasm");
Assert.FileExists(result, blazorPublishDirectory, "_framework", "wasm", "dotnet.js");
Assert.FileCountEquals(result, 1, Path.Combine(blazorPublishDirectory, "_framework", "wasm"), "dotnet.*.js");
Assert.FileExists(result, blazorPublishDirectory, "_framework", "_bin", "standalone.dll");
Assert.FileExists(result, blazorPublishDirectory, "_framework", "_bin", "Microsoft.Extensions.Logging.Abstractions.dll"); // Verify dependencies are part of the output.
@ -164,7 +164,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Build
Assert.FileExists(result, blazorPublishDirectory, "_framework", "blazor.boot.json");
Assert.FileExists(result, blazorPublishDirectory, "_framework", "blazor.webassembly.js");
Assert.FileExists(result, blazorPublishDirectory, "_framework", "wasm", "dotnet.wasm");
Assert.FileExists(result, blazorPublishDirectory, "_framework", "wasm", "dotnet.js");
Assert.FileCountEquals(result, 1, Path.Combine(blazorPublishDirectory, "_framework", "wasm"), "dotnet.*.js");
Assert.FileExists(result, blazorPublishDirectory, "_framework", "_bin", "standalone.dll");
Assert.FileExists(result, blazorPublishDirectory, "_framework", "_bin", "Microsoft.Extensions.Logging.Abstractions.dll"); // Verify dependencies are part of the output.
@ -208,7 +208,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Build
Assert.FileExists(result, blazorPublishDirectory, "_framework", "blazor.boot.json");
Assert.FileExists(result, blazorPublishDirectory, "_framework", "blazor.webassembly.js");
Assert.FileExists(result, blazorPublishDirectory, "_framework", "wasm", "dotnet.wasm");
Assert.FileExists(result, blazorPublishDirectory, "_framework", "wasm", "dotnet.js");
Assert.FileCountEquals(result, 1, Path.Combine(blazorPublishDirectory, "_framework", "wasm"), "dotnet.*.js");
Assert.FileExists(result, blazorPublishDirectory, "_framework", "_bin", "standalone.dll");
Assert.FileExists(result, blazorPublishDirectory, "_framework", "_bin", "Microsoft.Extensions.Logging.Abstractions.dll"); // Verify dependencies are part of the output.
@ -254,7 +254,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Build
Assert.FileExists(result, blazorPublishDirectory, "_framework", "blazor.boot.json");
Assert.FileExists(result, blazorPublishDirectory, "_framework", "blazor.webassembly.js");
Assert.FileExists(result, blazorPublishDirectory, "_framework", "wasm", "dotnet.wasm");
Assert.FileExists(result, blazorPublishDirectory, "_framework", "wasm", "dotnet.js");
Assert.FileCountEquals(result, 1, Path.Combine(blazorPublishDirectory, "_framework", "wasm"), "dotnet.*.js");
Assert.FileExists(result, blazorPublishDirectory, "_framework", "_bin", "standalone.dll");
Assert.FileExists(result, blazorPublishDirectory, "_framework", "_bin", "Microsoft.Extensions.Logging.Abstractions.dll"); // Verify dependencies are part of the output.

View File

@ -41,10 +41,10 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Build
fileHash: "pdbhashpdbhashpdbhash"),
CreateResourceTaskItem(
ResourceType.wasm,
itemSpec: "some-wasm-file",
ResourceType.runtime,
itemSpec: "some-runtime-file",
relativeOutputPath: null,
fileHash: "wasmhashwasmhashwasmhash")
fileHash: "runtimehashruntimehash")
}
};
@ -75,9 +75,9 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Build
resourceListKey =>
{
var resources = parsedContent.resources[resourceListKey];
Assert.Equal(ResourceType.wasm, resourceListKey);
Assert.Equal(ResourceType.runtime, resourceListKey);
Assert.Single(resources);
Assert.Equal("sha256-wasmhashwasmhashwasmhash", resources["some-wasm-file"]);
Assert.Equal("sha256-runtimehashruntimehash", resources["some-runtime-file"]);
});
}
@ -145,6 +145,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Build
{
var mock = new Mock<ITaskItem>();
mock.Setup(m => m.ItemSpec).Returns(itemSpec);
mock.Setup(m => m.GetMetadata("TargetOutputPath")).Returns(itemSpec);
mock.Setup(m => m.GetMetadata("BootResourceType")).Returns(type.ToString());
mock.Setup(m => m.GetMetadata("RelativeOutputPath")).Returns(relativeOutputPath);
mock.Setup(m => m.GetMetadata("FileHash")).Returns(fileHash);

View File

@ -110,14 +110,18 @@
}
}
function addScriptTagsToDocument() {
async function addScriptTagsToDocument() {
var browserSupportsNativeWebAssembly = typeof WebAssembly !== 'undefined' && WebAssembly.validate;
if (!browserSupportsNativeWebAssembly) {
throw new Error('This browser does not support WebAssembly.');
}
var bootJson = await fetch('/_framework/blazor.boot.json').then(res => res.json());
var dotNetJsResourceName = Object.keys(bootJson.resources.runtime)
.filter(name => name.endsWith('.js'));
var scriptElem = document.createElement('script');
scriptElem.src = '/_framework/wasm/dotnet.js';
scriptElem.src = '/_framework/wasm/' + dotNetJsResourceName;
document.body.appendChild(scriptElem);
}