Make index.html static again (#1123)

* Add build command for generating the new boot JSON file

* Remove build command for generating index.html

* Update build targets to generate blazor.boot.json

* Change SPA fallback routing to look for index.html in regular wwwroot. Will need to update setup for published apps later.

* Stop autorebuild when index.html changes, since we no longer 'build' that file

* Update Boot.WebAssembly.ts logic to use boot JSON info on startup

* Restore support for loading CSS/JS from Blazor library projects

* Use new startup script tag in all samples and templates

* Fix MonoSanity sample - it must now be an exe because we use that as the trigger to generate the boot json (not the presence of index.html any more)

* Fix SPA fallback routing for published apps

Because in a previous commit, I changed it to look for index.html inside "wwwroot" instead of "dist" (because we no longer build it). But "wwwroot" doesn't exist for published apps, because static file servers wouldn't understand it.

* CR: Fix path normalization
This commit is contained in:
Steve Sanderson 2018-07-13 09:49:34 +01:00 committed by GitHub
parent d9aafb05c1
commit ad7e98be04
23 changed files with 332 additions and 494 deletions

View File

@ -1,4 +1,4 @@
<!DOCTYPE html>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
@ -7,6 +7,6 @@
<body>
<app>Loading...</app>
<script src="customJsFileForTests.js"></script>
<script type="blazor-boot"></script>
<script src="_framework/blazor.webassembly.js"></script>
</body>
</html>

View File

@ -65,7 +65,6 @@
<p id="loadingIndicator">Loading...</p>
<script type="blazor-boot"></script>
<script src="loader.js"></script>
<script>
initMono(['_framework/_bin/MonoSanityClient.dll', '_framework/_bin/MonoSanityClient.pdb'], function () {

View File

@ -1,9 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk.Razor" TreatAsLocalProperty="BlazorLinkOnBuild">
<Project Sdk="Microsoft.NET.Sdk.Razor" TreatAsLocalProperty="BlazorLinkOnBuild">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<IsPackable>false</IsPackable>
<BlazorLinkOnBuild>false</BlazorLinkOnBuild>
<OutputType>exe</OutputType>
<!-- loader.js is hard-coded to assume it can load .pdbs regardless of Debug/Release configuration -->
<BlazorEnableDebugging>true</BlazorEnableDebugging>

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// 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.
namespace MonoSanityClient
@ -8,5 +8,8 @@ namespace MonoSanityClient
public class Program
{
static void Main()
{
}
}
}

View File

@ -1,4 +1,4 @@
<!DOCTYPE html>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
@ -11,6 +11,6 @@
<body>
<app>Loading...</app>
<script type="blazor-boot"></script>
<script src="_framework/blazor.webassembly.js"></script>
</body>
</html>

View File

@ -15,26 +15,20 @@ async function boot() {
renderBatch(browserRendererId, new SharedMemoryRenderBatch(batchAddress));
};
// Read startup config from the <script> element that's importing this file
const allScriptElems = document.getElementsByTagName('script');
const thisScriptElem = (document.currentScript || allScriptElems[allScriptElems.length - 1]) as HTMLScriptElement;
const isLinkerEnabled = thisScriptElem.getAttribute('linker-enabled') === 'true';
const entryPointDll = getRequiredBootScriptAttribute(thisScriptElem, 'main');
const entryPointMethod = getRequiredBootScriptAttribute(thisScriptElem, 'entrypoint');
const entryPointAssemblyName = getAssemblyNameFromUrl(entryPointDll);
const referenceAssembliesCommaSeparated = thisScriptElem.getAttribute('references') || '';
const referenceAssemblies = referenceAssembliesCommaSeparated
.split(',')
.map(s => s.trim())
.filter(s => !!s);
// Fetch the boot JSON file
// Later we might make the location of this configurable (e.g., as an attribute on the <script>
// element that's importing this file), but currently there isn't a use case for that.
const bootConfigResponse = await fetch('_framework/blazor.boot.json');
const bootConfig: BootJsonData = await bootConfigResponse.json();
const embeddedResourcePromises = startLoadingEmbeddedResources(bootConfig);
if (!isLinkerEnabled) {
if (!bootConfig.linkerEnabled) {
console.info('Blazor is running in dev mode without IL stripping. To make the bundle size significantly smaller, publish the application or see https://go.microsoft.com/fwlink/?linkid=870414');
}
// Determine the URLs of the assemblies we want to load
const loadAssemblyUrls = [entryPointDll]
.concat(referenceAssemblies)
// Determine the URLs of the assemblies we want to load, then begin fetching them all
const loadAssemblyUrls = [bootConfig.main]
.concat(bootConfig.assemblyReferences)
.map(filename => `_framework/_bin/${filename}`);
try {
@ -43,16 +37,45 @@ async function boot() {
throw new Error(`Failed to start platform. Reason: ${ex}`);
}
// Before we start running .NET code, be sure embedded content resources are all loaded
await Promise.all(embeddedResourcePromises)
// Start up the application
platform.callEntryPoint(entryPointAssemblyName, entryPointMethod, []);
const mainAssemblyName = getAssemblyNameFromUrl(bootConfig.main);
platform.callEntryPoint(mainAssemblyName, bootConfig.entryPoint, []);
}
function getRequiredBootScriptAttribute(elem: HTMLScriptElement, attributeName: string): string {
const result = elem.getAttribute(attributeName);
if (!result) {
throw new Error(`Missing "${attributeName}" attribute on Blazor script tag.`);
}
return result;
function startLoadingEmbeddedResources(bootConfig: BootJsonData) {
const cssLoadingPromises = bootConfig.cssReferences.map(cssReference => {
const linkElement = document.createElement('link');
linkElement.rel = 'stylesheet';
linkElement.href = cssReference;
return loadResourceFromElement(linkElement);
});
const jsLoadingPromises = bootConfig.jsReferences.map(jsReference => {
const scriptElement = document.createElement('script');
scriptElement.src = jsReference;
return loadResourceFromElement(scriptElement);
});
return cssLoadingPromises.concat(jsLoadingPromises);
}
function loadResourceFromElement(element: HTMLElement) {
return new Promise((resolve, reject) => {
element.onload = resolve;
element.onerror = reject;
document.head.appendChild(element);
});
}
// Keep in sync with BootJsonData in Microsoft.AspNetCore.Blazor.Build
interface BootJsonData {
main: string;
entryPoint: string;
assemblyReferences: string[];
cssReferences: string[];
jsReferences: string[];
linkerEnabled: boolean;
}
boot();

View File

@ -188,7 +188,10 @@ function addScriptTagsToDocument() {
meminitXHR.send(null);
}
document.write(`<script defer src="${monoRuntimeScriptUrl}"></script>`);
const scriptElem = document.createElement('script');
scriptElem.src = monoRuntimeScriptUrl;
scriptElem.defer = true;
document.body.appendChild(scriptElem);
}
function createEmscriptenModuleInstance(loadAssemblyUrls: string[], onReady: () => void, onError: (reason?: any) => void) {

View File

@ -1,20 +1,16 @@
// Copyright (c) .NET Foundation. All rights reserved.
// 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 Microsoft.Extensions.CommandLineUtils;
using System;
using System.IO;
using Microsoft.Extensions.CommandLineUtils;
namespace Microsoft.AspNetCore.Blazor.Build.Cli.Commands
{
internal class BuildIndexHtmlCommand
internal class WriteBootJsonCommand
{
public static void Command(CommandLineApplication command)
{
var clientPage = command.Option("--html-page",
"Path to the HTML Page containing the Blazor bootstrap script tag.",
CommandOptionType.SingleValue);
var referencesFile = command.Option("--references",
"The path to a file that lists the paths to given referenced dll files",
CommandOptionType.SingleValue);
@ -36,8 +32,7 @@ namespace Microsoft.AspNetCore.Blazor.Build.Cli.Commands
command.OnExecute(() =>
{
if (string.IsNullOrEmpty(mainAssemblyPath.Value) ||
!clientPage.HasValue() || !outputPath.HasValue())
if (string.IsNullOrEmpty(mainAssemblyPath.Value) || !outputPath.HasValue())
{
command.ShowHelp(command.Name);
return 1;
@ -53,8 +48,7 @@ namespace Microsoft.AspNetCore.Blazor.Build.Cli.Commands
? File.ReadAllLines(embeddedResourcesFile.Value())
: Array.Empty<string>();
IndexHtmlWriter.UpdateIndex(
clientPage.Value(),
BootJsonWriter.WriteFile(
mainAssemblyPath.Value,
referencesSources,
embeddedResourcesSources,

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// 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 Microsoft.AspNetCore.Blazor.Build.Cli.Commands;
@ -15,9 +15,9 @@ namespace Microsoft.AspNetCore.Blazor.Build
Name = "Microsoft.AspNetCore.Blazor.Build"
};
app.HelpOption("-?|-h|--help");
app.Command("build", BuildIndexHtmlCommand.Command);
app.Command("resolve-dependencies", ResolveRuntimeDependenciesCommand.Command);
app.Command("write-boot-json", WriteBootJsonCommand.Command);
if (args.Length > 0)
{

View File

@ -0,0 +1,94 @@
// 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 Microsoft.JSInterop;
using Mono.Cecil;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace Microsoft.AspNetCore.Blazor.Build
{
internal class BootJsonWriter
{
public static void WriteFile(
string assemblyPath,
string[] assemblyReferences,
string[] embeddedResourcesSources,
bool linkerEnabled,
string outputPath)
{
var embeddedContent = EmbeddedResourcesProcessor.ExtractEmbeddedResources(
embeddedResourcesSources, Path.GetDirectoryName(outputPath));
var bootJsonText = GetBootJsonContent(
Path.GetFileName(assemblyPath),
GetAssemblyEntryPoint(assemblyPath),
assemblyReferences,
embeddedContent,
linkerEnabled);
var normalizedOutputPath = Path.GetFullPath(outputPath);
Console.WriteLine("Writing boot data to: " + normalizedOutputPath);
File.WriteAllText(normalizedOutputPath, bootJsonText);
}
public static string GetBootJsonContent(string assemblyFileName, string entryPoint, string[] assemblyReferences, IEnumerable<EmbeddedResourceInfo> embeddedContent, bool linkerEnabled)
{
var data = new BootJsonData(
assemblyFileName,
entryPoint,
assemblyReferences,
embeddedContent,
linkerEnabled);
return Json.Serialize(data);
}
private static string GetAssemblyEntryPoint(string assemblyPath)
{
using (var assemblyDefinition = AssemblyDefinition.ReadAssembly(assemblyPath))
{
var entryPoint = assemblyDefinition.EntryPoint;
if (entryPoint == null)
{
throw new ArgumentException($"The assembly at {assemblyPath} has no specified entry point.");
}
return $"{entryPoint.DeclaringType.FullName}::{entryPoint.Name}";
}
}
/// <summary>
/// Defines the structure of a Blazor boot JSON file
/// </summary>
class BootJsonData
{
public string Main { get; }
public string EntryPoint { get; }
public IEnumerable<string> AssemblyReferences { get; }
public IEnumerable<string> CssReferences { get; }
public IEnumerable<string> JsReferences { get; }
public bool LinkerEnabled { get; }
public BootJsonData(
string entrypointAssemblyWithExtension,
string entryPoint,
IEnumerable<string> assemblyReferences,
IEnumerable<EmbeddedResourceInfo> embeddedContent,
bool linkerEnabled)
{
Main = entrypointAssemblyWithExtension;
EntryPoint = entryPoint;
AssemblyReferences = assemblyReferences;
LinkerEnabled = linkerEnabled;
CssReferences = embeddedContent
.Where(c => c.Kind == EmbeddedResourceKind.Css)
.Select(c => c.RelativePath);
JsReferences = embeddedContent
.Where(c => c.Kind == EmbeddedResourceKind.JavaScript)
.Select(c => c.RelativePath);
}
}
}
}

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// 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 Mono.Cecil;
@ -82,7 +82,7 @@ namespace Microsoft.AspNetCore.Blazor.Build
}
WriteResourceFile(embeddedResource, outputPath);
// The URLs we write into the index.html file need to use web-style directory separators
// The URLs we write into the boot json file need to use web-style directory separators
extractedResourceInfo = new EmbeddedResourceInfo(kind, EnsureHasPathSeparators(name, '/'));
return true;
}

View File

@ -1,254 +0,0 @@
// 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 AngleSharp;
using AngleSharp.Extensions;
using AngleSharp.Html;
using AngleSharp.Parser.Html;
using Mono.Cecil;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
namespace Microsoft.AspNetCore.Blazor.Build
{
internal class IndexHtmlWriter
{
public static void UpdateIndex(
string path,
string assemblyPath,
IEnumerable<string> assemblyReferences,
IEnumerable<string> embeddedResourcesSources,
bool linkerEnabled,
string outputPath)
{
var template = GetTemplate(path);
if (template == null)
{
return;
}
var assemblyName = Path.GetFileNameWithoutExtension(assemblyPath);
var entryPoint = GetAssemblyEntryPoint(assemblyPath);
var embeddedContent = EmbeddedResourcesProcessor.ExtractEmbeddedResources(
embeddedResourcesSources, Path.GetDirectoryName(outputPath));
var updatedContent = GetIndexHtmlContents(template, assemblyName, entryPoint, assemblyReferences, embeddedContent, linkerEnabled);
var normalizedOutputPath = Normalize(outputPath);
Console.WriteLine("Writing index to: " + normalizedOutputPath);
File.WriteAllText(normalizedOutputPath, updatedContent);
}
private static string Normalize(string outputPath) =>
Path.Combine(Path.GetDirectoryName(outputPath), Path.GetFileName(outputPath).ToLowerInvariant());
private static string GetTemplate(string path)
{
var fileName = Path.GetFileName(path);
if (File.Exists(path))
{
return File.ReadAllText(path);
}
if (Directory.Exists(Path.GetDirectoryName(path)))
{
var files = Directory.EnumerateFiles(Path.GetDirectoryName(path))
.OrderBy(f => f);
foreach (var file in files)
{
if (string.Equals(fileName, Path.GetFileName(file), StringComparison.OrdinalIgnoreCase))
{
return File.ReadAllText(file);
}
}
}
return null;
}
private static string GetAssemblyEntryPoint(string assemblyPath)
{
using (var assemblyDefinition = AssemblyDefinition.ReadAssembly(assemblyPath))
{
var entryPoint = assemblyDefinition.EntryPoint;
if (entryPoint == null)
{
throw new ArgumentException($"The assembly at {assemblyPath} has no specified entry point.");
}
return $"{entryPoint.DeclaringType.FullName}::{entryPoint.Name}";
}
}
/// <summary>
/// Injects the Blazor boot code and supporting config data at a user-designated
/// script tag identified with a <c>type</c> of <c>blazor-boot</c>.
/// </summary>
/// <remarks>
/// <para>
/// If a matching script tag is found, then it will be adjusted to inject
/// supporting configuration data, including a <c>src</c> attribute that
/// will load the Blazor client-side library. Any existing attribute
/// names that match the boot config data will be overwritten, but other
/// user-supplied attributes will be left intact. This allows, for example,
/// to designate asynchronous loading or deferred running of the script
/// reference.
/// </para><para>
/// If no matching script tag is found, it is assumed that the user is
/// responsible for completing the Blazor boot process.
/// </para>
/// </remarks>
public static string GetIndexHtmlContents(
string htmlTemplate,
string assemblyName,
string assemblyEntryPoint,
IEnumerable<string> assemblyReferences,
IEnumerable<EmbeddedResourceInfo> embeddedContent,
bool linkerEnabled)
{
var resultBuilder = new StringBuilder();
// Search for a tag of the form <script type="boot-blazor"></script>, and replace
// it with a fully-configured Blazor boot script tag
var textSource = new TextSource(htmlTemplate);
var currentRangeStartPos = 0;
var isInBlazorBootTag = false;
var resumeOnNextToken = false;
var result = string.Empty;
foreach (var token in textSource.Tokenize(HtmlEntityService.Resolver))
{
var tokenCharIndex = token.Position.Position - 1;
if (resumeOnNextToken)
{
resumeOnNextToken = false;
currentRangeStartPos = tokenCharIndex;
}
switch (token.Type)
{
case HtmlTokenType.StartTag:
{
// Only do anything special if this is a Blazor boot tag
var tag = token.AsTag();
if (IsBlazorBootTag(tag))
{
// First, emit the original source text prior to this special tag, since
// we want that to be unchanged
resultBuilder.Append(htmlTemplate, currentRangeStartPos, tokenCharIndex - currentRangeStartPos);
// Instead of emitting the source text for this special tag, emit a fully-
// configured Blazor boot script tag
AppendScriptTagWithBootConfig(
resultBuilder,
assemblyName,
assemblyEntryPoint,
assemblyReferences,
linkerEnabled,
tag.Attributes);
// Emit tags to reference any specified JS/CSS files
AppendReferenceTags(
resultBuilder,
embeddedContent.Where(c => c.Kind == EmbeddedResourceKind.Css).Select(c => c.RelativePath),
"<link rel=\"stylesheet\" href=\"{0}\" />");
AppendReferenceTags(
resultBuilder,
embeddedContent.Where(c => c.Kind == EmbeddedResourceKind.JavaScript).Select(c => c.RelativePath),
"<script src=\"{0}\" defer></script>");
// Set a flag so we know not to emit anything else until the special
// tag is closed
isInBlazorBootTag = true;
}
break;
}
case HtmlTokenType.EndTag:
// If this is an end tag corresponding to the Blazor boot script tag, we
// can switch back into the mode of emitting the original source text
if (isInBlazorBootTag)
{
isInBlazorBootTag = false;
resumeOnNextToken = true;
}
break;
case HtmlTokenType.EndOfFile:
// Finally, emit any remaining text from the original source file
var remainingLength = htmlTemplate.Length - currentRangeStartPos;
if (remainingLength > 0)
{
resultBuilder.Append(htmlTemplate, currentRangeStartPos, remainingLength);
}
result = resultBuilder.ToString();
break;
}
}
return result;
}
private static void AppendReferenceTags(StringBuilder resultBuilder, IEnumerable<string> urls, string format)
{
foreach (var url in urls)
{
resultBuilder.AppendLine();
resultBuilder.AppendFormat(format, url);
}
}
private static bool IsBlazorBootTag(HtmlTagToken tag)
=> string.Equals(tag.Name, "script", StringComparison.Ordinal)
&& tag.Attributes.Any(pair =>
string.Equals(pair.Key, "type", StringComparison.Ordinal)
&& string.Equals(pair.Value, "blazor-boot", StringComparison.Ordinal));
private static void AppendScriptTagWithBootConfig(
StringBuilder resultBuilder,
string assemblyName,
string assemblyEntryPoint,
IEnumerable<string> binFiles,
bool linkerEnabled,
List<KeyValuePair<string, string>> attributes)
{
var assemblyNameWithExtension = $"{assemblyName}.dll";
var referencesAttribute = string.Join(",", binFiles.ToArray());
var attributesDict = attributes.ToDictionary(x => x.Key, x => x.Value);
attributesDict.Remove("type");
attributesDict["src"] = "_framework/blazor.webassembly.js";
attributesDict["main"] = assemblyNameWithExtension;
attributesDict["entrypoint"] = assemblyEntryPoint;
attributesDict["references"] = referencesAttribute;
if (linkerEnabled)
{
attributesDict["linker-enabled"] = "true";
}
else
{
attributesDict.Remove("linker-enabled");
}
resultBuilder.Append("<script");
foreach (var attributePair in attributesDict)
{
if (!string.IsNullOrEmpty(attributePair.Value))
{
resultBuilder.AppendFormat(" {0}=\"{1}\"",
attributePair.Key,
attributePair.Value); // TODO: HTML attribute encode
}
else
{
resultBuilder.AppendFormat(" {0}",
attributePair.Key);
}
}
resultBuilder.Append("></script>");
}
}
}

View File

@ -46,7 +46,6 @@
<ProjectReference Include="..\Microsoft.AspNetCore.Blazor.Browser.JS\Microsoft.AspNetCore.Blazor.Browser.JS.csproj" />
<ProjectReference Include="..\Microsoft.AspNetCore.Blazor.Razor.Extensions\Microsoft.AspNetCore.Blazor.Razor.Extensions.csproj" />
<ProjectReference Include="..\Microsoft.AspNetCore.Blazor\Microsoft.AspNetCore.Blazor.csproj" />
<PackageReference Include="AngleSharp" Version="0.9.9.2" />
<!-- Intentionally include Razor.Design's MSBuild assets for everyone who reference Blazor.Build -->
<PackageReference Include="Microsoft.AspNetCore.Razor.Design" Version="$(RazorPackageVersion)" PrivateAssets="None" />

View File

@ -21,8 +21,8 @@
<BaseBlazorJsOutputPath>$(BaseBlazorRuntimeOutputPath)</BaseBlazorJsOutputPath>
<BaseBlazorIntermediateOutputPath>blazor/</BaseBlazorIntermediateOutputPath>
<BlazorWebRootName>wwwroot/</BlazorWebRootName>
<BlazorIndexHtmlName>Index.html</BlazorIndexHtmlName>
<BlazorOutputIndexHtmlName>$(BlazorIndexHtmlName.ToLowerInvariant())</BlazorOutputIndexHtmlName>
<BlazorBootJsonName>blazor.boot.json</BlazorBootJsonName>
<BlazorBootJsonOutputPath>$(BaseBlazorRuntimeOutputPath)$(BlazorBootJsonName)</BlazorBootJsonOutputPath>
</PropertyGroup>
<ItemGroup>

View File

@ -5,7 +5,8 @@
DependsOnTargets="PrepareBlazorOutputs"
Inputs="@(BlazorItemOutput)"
Outputs="@(BlazorItemOutput->'%(TargetOutputPath)')"
AfterTargets="CopyFilesToOutputDirectory">
AfterTargets="CopyFilesToOutputDirectory"
Condition="'$(OutputType.ToLowerInvariant())'=='exe'">
<!-- Copy the blazor output files -->
<Copy
@ -63,7 +64,7 @@
_ReadResolvedBlazorApplicationAssemblies
_IntermediateCopyBlazorApplicationAssemblies
_TouchBlazorApplicationAssemblies
_GenerateBlazorIndexHtml
_GenerateBlazorBootJson
_BlazorCopyFilesToOutputDirectory
The process for doing builds goes as follows:
@ -89,11 +90,11 @@
that updated versions of the files get copied to the output folder.
Once the binary outputs are resolved:
1) Create a marker file with the resolved assemblies and the index html file as inputs.
2) If the marker file is newer than the index.html in the output folder, regenerate the
index.html
1) Create a marker file with the resolved assemblies and the boot json data as inputs.
2) If the marker file is newer than the boot json in the output folder, regenerate the
boot json
Once all the outputs are resolved (static content + binary outputs + index.html)
Once all the outputs are resolved (static content + binary outputs + boot json)
Copy all the files to the output folder.
-->
@ -102,7 +103,7 @@
_PrepareBlazorOutputConfiguration;
_DefineBlazorCommonInputs;
_BlazorResolveOutputBinaries;
_GenerateBlazorIndexHtml;
_GenerateBlazorBootJson;
</PrepareBlazorOutputs>
</PropertyGroup>
@ -124,8 +125,7 @@
/_framework/asmjs <- This will contain the asmjs runtime copied from the nuget package.
/_framework/wasm <- This will contain the wsm runtime copied from the nuget package.
/_framework/blazor.js <- This is the blazor.js file copied from the nuget package.
/index.html <- This is the optional index.html file generated from wwwroot/Index.html in case it's present. It
will be canonicalized to index.html
/_framework/blazor.boot.json <- This is the boot json file
This task also defines some intermediate paths that we will use:
/obj/<<configuration>>/<<targetframework>>/blazor/blazor/linker <- This will be used to create the output from the linker.
@ -145,8 +145,8 @@
/obj/<<configuration>>/<<targetframework>>/blazor/resolvedassemblies/ <- This will be used to store the resolved assemblies
before copying them to the output when linking is not enabled.
/obj/<<configuration>>/<<targetframework>>/blazor/resolved.assemblies.txt <- This keeps track of all the resolved assemblies.
/obj/<<configuration>>/<<targetframework>>/blazor/index.html <- The generated index.html with the updated blazor script tag.
/obj/<<configuration>>/<<targetframework>>/blazor/inputs.index.cache <- The marker file that track whether index.html needs to
/obj/<<configuration>>/<<targetframework>>/blazor/blazor.boot.json <- The generated boot json file
/obj/<<configuration>>/<<targetframework>>/blazor/inputs.bootjson.cache <- The marker file that track whether boot json needs to
be regenerated.
-->
@ -156,11 +156,6 @@
<_BlazorBuiltInBclLinkerDescriptor>$(MSBuildThisFileDirectory)BuiltInBclLinkerDescriptor.xml</_BlazorBuiltInBclLinkerDescriptor>
</PropertyGroup>
<PropertyGroup Label="Blazor HTML inputs">
<BlazorWebRootPath>$(ProjectDir)$(BlazorWebRootName)</BlazorWebRootPath>
<BlazorIndexHtml>$(BlazorWebRootPath)$(BlazorIndexHtmlName)</BlazorIndexHtml>
</PropertyGroup>
<ItemGroup Label="Static content to copy to the output folder">
<BlazorItemOutput Include="@(MonoAsmjsFile)">
<TargetOutputPath>$(ProjectDir)$(OutputPath)$(BaseBlazorRuntimeAsmjsOutputPath)%(FileName)%(Extension)</TargetOutputPath>
@ -225,22 +220,22 @@
<!-- /obj/<<configuration>>/<<targetframework>>/blazor/resolved.assemblies.txt -->
<BlazorResolvedAssembliesOutputPath>$(BlazorIntermediateOutputPath)resolved.assemblies.txt</BlazorResolvedAssembliesOutputPath>
<!-- Index.html related paths and markers -->
<!-- boot json related paths and markers -->
<!-- /obj/<<configuration>>/<<targetframework>>/blazor/ -->
<BlazorIndexHtmlOutputDir>$(BlazorIntermediateOutputPath)</BlazorIndexHtmlOutputDir>
<BlazorBootJsonIntermediateOutputDir>$(BlazorIntermediateOutputPath)</BlazorBootJsonIntermediateOutputDir>
<!-- /obj/<<configuration>>/<<targetframework>>/blazor/index.html -->
<BlazorIndexHtmlOutputPath>$(BlazorIndexHtmlOutputDir)$(BlazorOutputIndexHtmlName)</BlazorIndexHtmlOutputPath>
<!-- /obj/<<configuration>>/<<targetframework>>/blazor/blazor.boot.json -->
<BlazorBootJsonIntermediateOutputPath>$(BlazorBootJsonIntermediateOutputDir)$(BlazorBootJsonName)</BlazorBootJsonIntermediateOutputPath>
<!-- /obj/<<configuration>>/<<targetframework>>/blazor/inputs.index.cache -->
<BlazorBuildIndexInputsCache>$(BlazorIntermediateOutputPath)inputs.index.cache</BlazorBuildIndexInputsCache>
<!-- /obj/<<configuration>>/<<targetframework>>/blazor/inputs.bootjson.cache -->
<BlazorBuildBootJsonInputsCache>$(BlazorIntermediateOutputPath)inputs.bootjson.cache</BlazorBuildBootJsonInputsCache>
<!-- /obj/<<configuration>>/<<targetframework>>/blazor/resolve-dependencies.txt -->
<BlazorResolveDependenciesFilePath>$(BlazorIntermediateOutputPath)resolve-dependencies.txt</BlazorResolveDependenciesFilePath>
<!-- /obj/<<configuration>>/<<targetframework>>/blazor/indexhtml-references.txt -->
<BlazorIndexHtmlReferencesFilePath>$(BlazorIntermediateOutputPath)indexhtml-references.txt</BlazorIndexHtmlReferencesFilePath>
<!-- /obj/<<configuration>>/<<targetframework>>/blazor/bootjson-references.txt -->
<BlazorBootJsonReferencesFilePath>$(BlazorIntermediateOutputPath)bootjson-references.txt</BlazorBootJsonReferencesFilePath>
<!-- /obj/<<configuration>>/<<targetframework>>/blazor/embedded.resources.txt -->
<BlazorEmbeddedResourcesConfigFilePath>$(BlazorIntermediateOutputPath)embedded.resources.txt</BlazorEmbeddedResourcesConfigFilePath>
@ -586,36 +581,35 @@
Final part of the build pipeline:
* Collect the blazor application assemblies to be copied to the output and create a marker file.
* Call our CLI tool to generate the index html if the list of assemblies has changed.
* Call our CLI tool to generate the boot json if the list of assemblies has changed.
-->
<Target Name="_ResolveBlazorIndexHtmlInputs">
<Target Name="_ResolveBlazorBootJsonInputs">
<ItemGroup>
<BlazorIndexHtmlInput Include="$(BlazorIndexHtml)" />
<BlazorIndexHtmlInput Include="$(Configuration)" />
<BlazorIndexHtmlInput Include="@(BlazorItemOutput->WithMetadataValue('Type','Assembly')->'%(FullPath)')" />
<BlazorIndexHtmlInput Include="@(BlazorItemOutput->WithMetadataValue('Type','Pdb')->'%(FullPath)')" />
<BlazorIndexHtmlInput Include="@(_BlazorLinkingOption)" />
<BlazorIndexHtmlInput Include="$(BlazorEnableDebugging)" />
<BlazorBootJsonInput Include="$(Configuration)" />
<BlazorBootJsonInput Include="@(BlazorItemOutput->WithMetadataValue('Type','Assembly')->'%(FullPath)')" />
<BlazorBootJsonInput Include="@(BlazorItemOutput->WithMetadataValue('Type','Pdb')->'%(FullPath)')" />
<BlazorBootJsonInput Include="@(_BlazorLinkingOption)" />
<BlazorBootJsonInput Include="$(BlazorEnableDebugging)" />
</ItemGroup>
<WriteLinesToFile
File="$(BlazorBuildIndexInputsCache)"
Lines="@(BlazorIndexHtmlInput)"
File="$(BlazorBuildBootJsonInputsCache)"
Lines="@(BlazorBootJsonInput)"
Overwrite="true"
WriteOnlyWhenDifferent="True" />
<ItemGroup>
<FileWrites Include="$(BlazorBuildIndexInputsCache)" />
<FileWrites Include="$(BlazorBuildBootJsonInputsCache)" />
</ItemGroup>
</Target>
<Target
Name="_GenerateBlazorIndexHtml"
DependsOnTargets="_ResolveBlazorIndexHtmlInputs"
Inputs="$(BlazorBuildIndexInputsCache);$(BlazorIndexHtml);@(_BlazorDependencyInput)"
Outputs="$(BlazorIndexHtmlOutputPath)">
Name="_GenerateBlazorBootJson"
DependsOnTargets="_ResolveBlazorBootJsonInputs"
Inputs="$(BlazorBuildBootJsonInputsCache);@(_BlazorDependencyInput)"
Outputs="$(BlazorBootJsonIntermediateOutputPath)">
<ItemGroup>
<_UnlinkedAppReferencesPaths Include="@(_BlazorDependencyInput)" />
<_AppReferences Include="@(BlazorItemOutput->WithMetadataValue('Type','Assembly')->WithMetadataValue('PrimaryOutput','')->'%(FileName)%(Extension)')" />
@ -623,12 +617,12 @@
</ItemGroup>
<PropertyGroup>
<_LinkerEnabledFlag Condition="'$(_BlazorShouldLinkApplicationAssemblies)' != ''">--linker-enabled</_LinkerEnabledFlag>
<_ReferencesArg Condition="'@(_AppReferences)' != ''">--references &quot;$(BlazorIndexHtmlReferencesFilePath)&quot;</_ReferencesArg>
<_ReferencesArg Condition="'@(_AppReferences)' != ''">--references &quot;$(BlazorBootJsonReferencesFilePath)&quot;</_ReferencesArg>
<_EmbeddedResourcesArg Condition="'@(_UnlinkedAppReferencesPaths)' != ''">--embedded-resources &quot;$(BlazorEmbeddedResourcesConfigFilePath)&quot;</_EmbeddedResourcesArg>
</PropertyGroup>
<WriteLinesToFile
File="$(BlazorIndexHtmlReferencesFilePath)"
File="$(BlazorBootJsonReferencesFilePath)"
Lines="@(_AppReferences)"
Overwrite="true" />
@ -638,20 +632,20 @@
Lines="@(_UnlinkedAppReferencesPaths)"
Overwrite="true" />
<Exec Command="$(BlazorBuildExe) build @(IntermediateAssembly) --html-page &quot;$(BlazorIndexHtml)&quot; $(_ReferencesArg) $(_EmbeddedResourcesArg) $(_LinkerEnabledFlag) --output &quot;$(BlazorIndexHtmlOutputPath)&quot;" />
<Exec Command="$(BlazorBuildExe) write-boot-json @(IntermediateAssembly) $(_ReferencesArg) $(_EmbeddedResourcesArg) $(_LinkerEnabledFlag) --output &quot;$(BlazorBootJsonIntermediateOutputPath)&quot;" />
<ItemGroup Condition="Exists('$(BlazorIndexHtmlOutputPath)')">
<_BlazorIndex Include="$(BlazorIndexHtmlOutputPath)" />
<_BlazorIndexEmbeddedContentFile Include="$(BlazorIndexHtmlOutputDir)_content\**\*.*" />
<BlazorItemOutput Include="@(_BlazorIndex)">
<TargetOutputPath>$(ProjectDir)$(OutputPath)dist/%(FileName)%(Extension)</TargetOutputPath>
<Type>EntryPoint</Type>
<ItemGroup Condition="Exists('$(BlazorBootJsonIntermediateOutputPath)')">
<_BlazorBootJson Include="$(BlazorBootJsonIntermediateOutputPath)" />
<_BlazorBootJsonEmbeddedContentFile Include="$(BlazorBootJsonIntermediateOutputDir)_content\**\*.*" />
<BlazorItemOutput Include="@(_BlazorBootJson)">
<TargetOutputPath>$(ProjectDir)$(OutputPath)$(BlazorBootJsonOutputPath)</TargetOutputPath>
<Type>BootJson</Type>
</BlazorItemOutput>
<BlazorItemOutput Include="@(_BlazorIndexEmbeddedContentFile)">
<BlazorItemOutput Include="@(_BlazorBootJsonEmbeddedContentFile)">
<TargetOutputPath>$(ProjectDir)$(OutputPath)dist/_content/%(RecursiveDir)%(FileName)%(Extension)</TargetOutputPath>
</BlazorItemOutput>
<FileWrites Include="$(BlazorIndexHtmlOutputPath)" />
<FileWrites Include="@(_BlazorIndexEmbeddedContentFile)" />
<FileWrites Include="$(BlazorBootJsonIntermediateOutputPath)" />
<FileWrites Include="@(_BlazorBootJsonEmbeddedContentFile)" />
</ItemGroup>
</Target>

View File

@ -16,10 +16,9 @@ namespace Microsoft.AspNetCore.Builder
internal static class AutoRebuildExtensions
{
// Note that we don't need to watch typical static-file extensions (.css, .js, etc.)
// because anything in wwwroot is just served directly from disk on each reload. But
// as a special case, we do watch index.html because it needs compilation.
// because anything in wwwroot is just served directly from disk on each reload.
// TODO: Make the set of extensions and exclusions configurable in csproj
private static string[] _includedSuffixes = new[] { ".cs", ".cshtml", "index.html" };
private static string[] _includedSuffixes = new[] { ".cs", ".cshtml" };
private static string[] _excludedDirectories = new[] { "obj", "bin" };
// To ensure the FileSystemWatchers aren't collected, reference them

View File

@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Hosting;
using Microsoft.Net.Http.Headers;
using System.Net.Mime;
using System;
using System.IO;
namespace Microsoft.AspNetCore.Builder
{
@ -48,12 +49,6 @@ namespace Microsoft.AspNetCore.Builder
// hence all the path manipulation here. We shouldn't be hardcoding 'dist' here either.
var env = (IHostingEnvironment)app.ApplicationServices.GetService(typeof(IHostingEnvironment));
var config = BlazorConfig.Read(options.ClientAssemblyPath);
var distDirStaticFiles = new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(config.DistPath),
ContentTypeProvider = CreateContentTypeProvider(config.EnableDebugging),
OnPrepareResponse = SetCacheHeaders
};
if (env.IsDevelopment() && config.EnableAutoRebuilding)
{
@ -68,15 +63,21 @@ namespace Microsoft.AspNetCore.Builder
}
// First, match the request against files in the client app dist directory
app.UseStaticFiles(distDirStaticFiles);
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(config.DistPath),
ContentTypeProvider = CreateContentTypeProvider(config.EnableDebugging),
OnPrepareResponse = SetCacheHeaders
});
// Next, match the request against static files in wwwroot
// * Before publishing, we serve the wwwroot files directly from source
// (and don't require them to be copied into dist).
// In this case, WebRootPath will be nonempty if that directory exists.
// * After publishing, the wwwroot files are already copied to 'dist' and
// will be served by the above middleware, so we do nothing here.
// In this case, WebRootPath will be empty (the publish process sets this).
if (!string.IsNullOrEmpty(config.WebRootPath))
{
// In development, we serve the wwwroot files directly from source
// (and don't require them to be copied into dist).
// TODO: When publishing is implemented, have config.WebRootPath set
// to null so that it only serves files that were copied to dist
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(config.WebRootPath),
@ -94,13 +95,48 @@ namespace Microsoft.AspNetCore.Builder
// excluding /_framework/*)
app.MapWhen(IsNotFrameworkDir, childAppBuilder =>
{
var indexHtmlPath = FindIndexHtmlFile(config);
var indexHtmlStaticFileOptions = string.IsNullOrEmpty(indexHtmlPath)
? null : new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(Path.GetDirectoryName(indexHtmlPath)),
OnPrepareResponse = SetCacheHeaders
};
childAppBuilder.UseSpa(spa =>
{
spa.Options.DefaultPageStaticFileOptions = distDirStaticFiles;
spa.Options.DefaultPageStaticFileOptions = indexHtmlStaticFileOptions;
});
});
}
private static string FindIndexHtmlFile(BlazorConfig config)
{
// Before publishing, the client project may have a wwwroot directory.
// If so, and if it contains index.html, use that.
if (!string.IsNullOrEmpty(config.WebRootPath))
{
var wwwrootIndexHtmlPath = Path.Combine(config.WebRootPath, "index.html");
if (File.Exists(wwwrootIndexHtmlPath))
{
return wwwrootIndexHtmlPath;
}
}
// After publishing, the client project won't have a wwwroot directory.
// The contents from that dir will have been copied to "dist" during publish.
// So if "dist/index.html" now exists, use that.
var distIndexHtmlPath = Path.Combine(config.DistPath, "index.html");
if (File.Exists(distIndexHtmlPath))
{
return distIndexHtmlPath;
}
// Since there's no index.html, we'll use the default DefaultPageStaticFileOptions,
// hence we'll look for index.html in the host server app's wwwroot.
return null;
}
private static void SetCacheHeaders(StaticFileResponseContext ctx)
{
// By setting "Cache-Control: no-cache", we're allowing the browser to store

View File

@ -1,4 +1,4 @@
<!DOCTYPE html>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
@ -11,6 +11,6 @@
<body>
<app>Loading...</app>
<script type="blazor-boot"></script>
<script src="_framework/blazor.webassembly.js"></script>
</body>
</html>

View File

@ -1,4 +1,4 @@
<!DOCTYPE html>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
@ -11,6 +11,6 @@
<body>
<app>Loading...</app>
<script type="blazor-boot"></script>
<script src="_framework/blazor.webassembly.js"></script>
</body>
</html>

View File

@ -0,0 +1,62 @@
// 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 Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Linq;
using Xunit;
namespace Microsoft.AspNetCore.Blazor.Build.Test
{
public class BootJsonWriterTest
{
[Fact]
public void ProducesJsonReferencingAssemblyAndDependencies()
{
// Arrange/Act
var assemblyReferences = new string[] { "System.Abc.dll", "MyApp.ClassLib.dll", };
var content = BootJsonWriter.GetBootJsonContent(
"MyApp.Entrypoint.dll",
"MyNamespace.MyType::MyMethod",
assemblyReferences,
Enumerable.Empty<EmbeddedResourceInfo>(),
linkerEnabled: true);
// Assert
var parsedContent = JsonConvert.DeserializeObject<JObject>(content);
Assert.Equal("MyApp.Entrypoint.dll", parsedContent["main"].Value<string>());
Assert.Equal("MyNamespace.MyType::MyMethod", parsedContent["entryPoint"].Value<string>());
Assert.Equal(assemblyReferences, parsedContent["assemblyReferences"].Values<string>());
}
[Fact]
public void IncludesReferencesToEmbeddedContent()
{
// Arrange/Act
var embeddedContent = new[]
{
new EmbeddedResourceInfo(EmbeddedResourceKind.Static, "my/static/file"),
new EmbeddedResourceInfo(EmbeddedResourceKind.Css, "css/first.css"),
new EmbeddedResourceInfo(EmbeddedResourceKind.JavaScript, "javascript/first.js"),
new EmbeddedResourceInfo(EmbeddedResourceKind.Css, "css/second.css"),
new EmbeddedResourceInfo(EmbeddedResourceKind.JavaScript, "javascript/second.js"),
};
var content = BootJsonWriter.GetBootJsonContent(
"MyApp.Entrypoint",
"MyNamespace.MyType::MyMethod",
assemblyReferences: new[] { "Something.dll" },
embeddedContent: embeddedContent,
linkerEnabled: true);
// Assert
var parsedContent = JsonConvert.DeserializeObject<JObject>(content);
Assert.Equal(
new[] { "css/first.css", "css/second.css" },
parsedContent["cssReferences"].Values<string>());
Assert.Equal(
new[] { "javascript/first.js", "javascript/second.js" },
parsedContent["jsReferences"].Values<string>());
}
}
}

View File

@ -1,116 +0,0 @@
// 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 AngleSharp.Parser.Html;
using System;
using System.Linq;
using Xunit;
namespace Microsoft.AspNetCore.Blazor.Build.Test
{
public class IndexHtmlWriterTest
{
[Fact]
public void InjectsScriptTagReferencingAssemblyAndDependencies()
{
// Arrange
var htmlTemplatePrefix = @"
<html>
<body>
<h1>Hello</h1>
Some text
<script>alert(1)</script>";
var htmlTemplateSuffix = @"
</body>
</html>";
var htmlTemplate =
$@"{htmlTemplatePrefix}
<script type='blazor-boot' custom1 custom2=""value"">some text that should be removed</script>
{htmlTemplateSuffix}";
var assemblyReferences = new string[] { "System.Abc.dll", "MyApp.ClassLib.dll", };
var instance = IndexHtmlWriter.GetIndexHtmlContents(
htmlTemplate,
"MyApp.Entrypoint",
"MyNamespace.MyType::MyMethod",
assemblyReferences,
Enumerable.Empty<EmbeddedResourceInfo>(),
linkerEnabled: true);
// Act & Assert: Start and end is not modified (including formatting)
Assert.StartsWith(htmlTemplatePrefix, instance);
Assert.EndsWith(htmlTemplateSuffix, instance);
// Assert: Boot tag is correct
var scriptTagText = instance.Substring(htmlTemplatePrefix.Length, instance.Length - htmlTemplatePrefix.Length - htmlTemplateSuffix.Length);
var parsedHtml = new HtmlParser().Parse("<html><body>" + scriptTagText + "</body></html>");
var scriptElems = parsedHtml.Body.QuerySelectorAll("script");
var linkElems = parsedHtml.Body.QuerySelectorAll("link");
var scriptElem = scriptElems[0];
Assert.False(scriptElem.HasChildNodes);
Assert.Equal("_framework/blazor.webassembly.js", scriptElem.GetAttribute("src"));
Assert.Equal("MyApp.Entrypoint.dll", scriptElem.GetAttribute("main"));
Assert.Equal("MyNamespace.MyType::MyMethod", scriptElem.GetAttribute("entrypoint"));
Assert.Equal("System.Abc.dll,MyApp.ClassLib.dll", scriptElem.GetAttribute("references"));
Assert.False(scriptElem.HasAttribute("type"));
Assert.Equal(string.Empty, scriptElem.Attributes["custom1"].Value);
Assert.Equal("value", scriptElem.Attributes["custom2"].Value);
Assert.Equal("true", scriptElem.Attributes["linker-enabled"].Value);
}
[Fact]
public void SuppliesHtmlTemplateUnchangedIfNoBootScriptPresent()
{
// Arrange
var htmlTemplate = "<!DOCTYPE html><html><body><h1 style='color:red'>Hello</h1>Some text<script type='irrelevant'>blah</script></body></html>";
var assemblyReferences = new string[] { "System.Abc.dll", "MyApp.ClassLib.dll" };
var jsReferences = new string[] { "some/file.js", "another.js" };
var cssReferences = new string[] { "my/styles.css" };
var content = IndexHtmlWriter.GetIndexHtmlContents(
htmlTemplate, "MyApp.Entrypoint", "MyNamespace.MyType::MyMethod", assemblyReferences, Enumerable.Empty<EmbeddedResourceInfo>(), linkerEnabled: true);
// Assert
Assert.Equal(htmlTemplate, content);
}
[Fact]
public void InjectsAdditionalTagsForEmbeddedContent()
{
// Arrange
var htmlTemplate = "Start <script id='testboot' type='blazor-boot'></script> End";
var embeddedContent = new[]
{
new EmbeddedResourceInfo(EmbeddedResourceKind.Static, "my/static/file"),
new EmbeddedResourceInfo(EmbeddedResourceKind.Css, "css/first.css"),
new EmbeddedResourceInfo(EmbeddedResourceKind.JavaScript, "javascript/first.js"),
new EmbeddedResourceInfo(EmbeddedResourceKind.Css, "css/second.css"),
new EmbeddedResourceInfo(EmbeddedResourceKind.JavaScript, "javascript/second.js"),
};
// Act
var resultHtml = IndexHtmlWriter.GetIndexHtmlContents(
htmlTemplate,
"MyApp.Entrypoint",
"MyNamespace.MyType::MyMethod",
assemblyReferences: new[] { "Something.dll" },
embeddedContent: embeddedContent,
linkerEnabled: true);
// Assert
var parsedHtml = new HtmlParser().Parse(resultHtml);
var blazorBootScript = parsedHtml.GetElementById("testboot");
Assert.NotNull(blazorBootScript);
Assert.Equal(
"Start "
+ blazorBootScript.OuterHtml
// First we insert the CSS file tags in order
+ Environment.NewLine + "<link rel=\"stylesheet\" href=\"css/first.css\" />"
+ Environment.NewLine + "<link rel=\"stylesheet\" href=\"css/second.css\" />"
// Then the JS file tags in order, each with 'defer'
+ Environment.NewLine + "<script src=\"javascript/first.js\" defer></script>"
+ Environment.NewLine + "<script src=\"javascript/second.js\" defer></script>"
+ " End",
resultHtml);
}
}
}

View File

@ -19,7 +19,6 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AngleSharp" Version="0.9.9.2" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="2.8.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.5.0" />
<PackageReference Include="xunit" Version="2.3.1" />

View File

@ -47,9 +47,11 @@
<app>Loading...</app>
<script type="blazor-boot"></script>
<script src="_framework/blazor.webassembly.js"></script>
<!-- Used for testing interop scenarios between JS and .NET -->
<script type="text/javascript" src="js/jsinteroptests.js"></script>
<script src="js/jsinteroptests.js"></script>
<script>
// The client-side .NET code calls this when it is ready to be called from test code
// The Xunit test code polls until it sees the flag is set