Support components and static content in external NuGet packages (#247)

* On build, drop <BlazorPackageContentFiles> items into dist\_content\(PackageName)\

* Add <script> and <link> tags to generated index.html

* Add testapp coverage of external content package. Still need to add E2E tests that uses it.

* Add missing unit test update

* Add example of packaging an entire Blazor component including CSS and images

* Add E2E test for component from NuGet package
This commit is contained in:
Steve Sanderson 2018-03-13 11:28:23 +00:00
parent 9549dccc54
commit 0e9d52fe11
17 changed files with 265 additions and 19 deletions

View File

@ -87,6 +87,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.VisualStudio.Lang
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.VisualStudio.BlazorExtension", "tooling\Microsoft.VisualStudio.BlazorExtension\Microsoft.VisualStudio.BlazorExtension.csproj", "{9088E4E4-B855-457F-AE9E-D86709A5E1F4}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestContentPackage", "test\testapps\TestContentPackage\TestContentPackage.csproj", "{C57382BC-EE93-49D5-BC40-5C98AF8AA048}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -301,6 +303,14 @@ Global
{9088E4E4-B855-457F-AE9E-D86709A5E1F4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9088E4E4-B855-457F-AE9E-D86709A5E1F4}.Release|Any CPU.Build.0 = Release|Any CPU
{9088E4E4-B855-457F-AE9E-D86709A5E1F4}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU
{C57382BC-EE93-49D5-BC40-5C98AF8AA048}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C57382BC-EE93-49D5-BC40-5C98AF8AA048}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C57382BC-EE93-49D5-BC40-5C98AF8AA048}.DebugNoVSIX|Any CPU.ActiveCfg = Debug|Any CPU
{C57382BC-EE93-49D5-BC40-5C98AF8AA048}.DebugNoVSIX|Any CPU.Build.0 = Debug|Any CPU
{C57382BC-EE93-49D5-BC40-5C98AF8AA048}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C57382BC-EE93-49D5-BC40-5C98AF8AA048}.Release|Any CPU.Build.0 = Release|Any CPU
{C57382BC-EE93-49D5-BC40-5C98AF8AA048}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU
{C57382BC-EE93-49D5-BC40-5C98AF8AA048}.ReleaseNoVSIX|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -340,6 +350,7 @@ Global
{FF25111E-5A3E-48A3-96D8-08A2C5A2A91C} = {ADA3AE29-F6DE-49F6-8C7C-B321508CAE8E}
{43E39257-7DC1-46BD-9BD9-2319A1313D07} = {F563ABB6-85FB-4CFC-B0D2-1D5130E8246D}
{9088E4E4-B855-457F-AE9E-D86709A5E1F4} = {F563ABB6-85FB-4CFC-B0D2-1D5130E8246D}
{C57382BC-EE93-49D5-BC40-5C98AF8AA048} = {4AE0D35B-D97A-44D0-8392-C9240377DCCE}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {504DA352-6788-4DC0-8705-82167E72A4D3}

View File

@ -18,6 +18,14 @@ namespace Microsoft.AspNetCore.Blazor.Build.Cli.Commands
"The path from the _bin folder to a given referenced dll file (Typically just the dll name)",
CommandOptionType.MultipleValue);
var jsReferences = command.Option("--js",
"Adds a <script> tag with the specified 'src' value",
CommandOptionType.MultipleValue);
var cssReferences = command.Option("--css",
"Adds a <link rel=stylesheet> tag with the specified 'href' value",
CommandOptionType.MultipleValue);
var outputPath = command.Option("--output",
"Path to the output file",
CommandOptionType.SingleValue);
@ -36,7 +44,13 @@ namespace Microsoft.AspNetCore.Blazor.Build.Cli.Commands
try
{
IndexHtmlWriter.UpdateIndex(clientPage.Value(), mainAssemblyPath.Value, references.Values.ToArray(), outputPath.Value());
IndexHtmlWriter.UpdateIndex(
clientPage.Value(),
mainAssemblyPath.Value,
references.Values.ToArray(),
jsReferences.Values.ToArray(),
cssReferences.Values.ToArray(),
outputPath.Value());
return 0;
}
catch (Exception ex)

View File

@ -15,7 +15,13 @@ namespace Microsoft.AspNetCore.Blazor.Build
{
internal class IndexHtmlWriter
{
public static void UpdateIndex(string path, string assemblyPath, IEnumerable<string> references, string outputPath)
public static void UpdateIndex(
string path,
string assemblyPath,
IEnumerable<string> assemblyReferences,
IEnumerable<string> jsReferences,
IEnumerable<string> cssReferences,
string outputPath)
{
var template = GetTemplate(path);
if (template == null)
@ -24,7 +30,7 @@ namespace Microsoft.AspNetCore.Blazor.Build
}
var assemblyName = Path.GetFileNameWithoutExtension(assemblyPath);
var entryPoint = GetAssemblyEntryPoint(assemblyPath);
var updatedContent = GetIndexHtmlContents(template, assemblyName, entryPoint, references);
var updatedContent = GetIndexHtmlContents(template, assemblyName, entryPoint, assemblyReferences, jsReferences, cssReferences);
var normalizedOutputPath = Normalize(outputPath);
Console.WriteLine("Writing index to: " + normalizedOutputPath);
File.WriteAllText(normalizedOutputPath, updatedContent);
@ -93,7 +99,9 @@ namespace Microsoft.AspNetCore.Blazor.Build
string htmlTemplate,
string assemblyName,
string assemblyEntryPoint,
IEnumerable<string> binFiles)
IEnumerable<string> assemblyReferences,
IEnumerable<string> jsReferences,
IEnumerable<string> cssReferences)
{
var resultBuilder = new StringBuilder();
@ -132,9 +140,19 @@ namespace Microsoft.AspNetCore.Blazor.Build
resultBuilder,
assemblyName,
assemblyEntryPoint,
binFiles,
assemblyReferences,
tag.Attributes);
// Emit tags to reference any specified JS/CSS files
AppendReferenceTags(
resultBuilder,
cssReferences,
"<link rel=\"stylesheet\" href=\"{0}\" />");
AppendReferenceTags(
resultBuilder,
jsReferences,
"<script src=\"{0}\" defer></script>");
// Set a flag so we know not to emit anything else until the special
// tag is closed
isInBlazorBootTag = true;
@ -160,6 +178,15 @@ namespace Microsoft.AspNetCore.Blazor.Build
}
}
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 =>

View File

@ -11,6 +11,7 @@
<PropertyGroup Label="Blazor build outputs">
<AdditionalMonoLinkerOptions>-c link -u link -t --verbose </AdditionalMonoLinkerOptions>
<BaseBlazorPackageContentOutputPath>dist/_content/</BaseBlazorPackageContentOutputPath>
<BaseBlazorRuntimeOutputPath>dist/_framework/</BaseBlazorRuntimeOutputPath>
<BaseBlazorRuntimeBinOutputPath>$(BaseBlazorRuntimeOutputPath)_bin/</BaseBlazorRuntimeBinOutputPath>
<BaseBlazorRuntimeAsmjsOutputPath>$(BaseBlazorRuntimeOutputPath)asmjs/</BaseBlazorRuntimeAsmjsOutputPath>

View File

@ -168,6 +168,14 @@
</BlazorItemOutput>
</ItemGroup>
<ItemGroup Label="Static content supplied by NuGet packages">
<_BlazorPackageContentOutput Include="@(BlazorPackageContentFile)" Condition="%(SourcePackage) != ''">
<TargetOutputPath>$(ProjectDir)$(OutputPath)$(BaseBlazorPackageContentOutputPath)%(SourcePackage)\%(RecursiveDir)\%(Filename)%(Extension)</TargetOutputPath>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</_BlazorPackageContentOutput>
<BlazorItemOutput Include="@(_BlazorPackageContentOutput)" />
</ItemGroup>
<PropertyGroup Label="Intermediate output paths">
<!-- /obj/<<configuration>>/<<targetframework>>/blazor -->
@ -522,6 +530,8 @@
<ItemGroup>
<BlazorIndexHtmlInput Include="$(BlazorIndexHtml)" />
<BlazorIndexHtmlInput Include="@(BlazorItemOutput->WithMetadataValue('Type','Assembly')->'%(FullPath)')" />
<BlazorIndexHtmlInput Include="@(BlazorPackageJsRef->'%(FullPath)')" />
<BlazorIndexHtmlInput Include="@(BlazorPackageCssRef->'%(FullPath)')" />
</ItemGroup>
<WriteLinesToFile
@ -543,9 +553,11 @@
Outputs="$(BlazorIndexHtmlOutputPath)">
<ItemGroup>
<_AppReferences Include="@(BlazorItemOutput->WithMetadataValue('Type','Assembly')->WithMetadataValue('PrimaryOutput','')->'%(FileName)%(Extension)')" />
<_JsReferences Include="@(BlazorPackageJsRef->'_content/%(SourcePackage)/%(RecursiveDir)%(FileName)%(Extension)')" />
<_CssReferences Include="@(BlazorPackageCssRef->'_content/%(SourcePackage)/%(RecursiveDir)%(FileName)%(Extension)')" />
</ItemGroup>
<Exec Command="$(BlazorBuildExe) build @(IntermediateAssembly) --html-page &quot;$(BlazorIndexHtml)&quot; @(_AppReferences->'--reference %(Identity)', ' ') --output &quot;$(BlazorIndexHtmlOutputPath)&quot;" />
<Exec Command="$(BlazorBuildExe) build @(IntermediateAssembly) --html-page &quot;$(BlazorIndexHtml)&quot; @(_AppReferences->'--reference %(Identity)', ' ') @(_JsReferences->'--js %(Identity)', ' ') @(_CssReferences->'--css %(Identity)', ' ') --output &quot;$(BlazorIndexHtmlOutputPath)&quot;" />
<ItemGroup Condition="Exists('$(BlazorIndexHtmlOutputPath)')">
<_BlazorIndex Include="$(BlazorIndexHtmlOutputPath)" />

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using AngleSharp.Parser.Html;
using System.Linq;
using Xunit;
namespace Microsoft.AspNetCore.Blazor.Build.Test
@ -25,15 +26,16 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
$@"{htmlTemplatePrefix}
<script type='blazor-boot' custom1 custom2=""value"">some text that should be removed</script>
{htmlTemplateSuffix}";
var dependencies = new string[]
{
"System.Abc.dll",
"MyApp.ClassLib.dll",
};
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 instance = IndexHtmlWriter.GetIndexHtmlContents(
htmlTemplate,
"MyApp.Entrypoint",
"MyNamespace.MyType::MyMethod", dependencies);
"MyNamespace.MyType::MyMethod",
assemblyReferences,
jsReferences,
cssReferences);
// Act & Assert: Start and end is not modified (including formatting)
Assert.StartsWith(htmlTemplatePrefix, instance);
@ -42,7 +44,9 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
// 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 scriptElem = parsedHtml.Body.QuerySelector("script");
var scriptElems = parsedHtml.Body.QuerySelectorAll("script");
var linkElems = parsedHtml.Body.QuerySelectorAll("link");
var scriptElem = scriptElems[0];
Assert.False(scriptElem.HasChildNodes);
Assert.Equal("_framework/blazor.js", scriptElem.GetAttribute("src"));
Assert.Equal("MyApp.Entrypoint.dll", scriptElem.GetAttribute("main"));
@ -51,6 +55,16 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
Assert.False(scriptElem.HasAttribute("type"));
Assert.Equal(string.Empty, scriptElem.Attributes["custom1"].Value);
Assert.Equal("value", scriptElem.Attributes["custom2"].Value);
// Assert: Also contains script tags referencing JS files
Assert.Equal(
scriptElems.Skip(1).Select(tag => tag.GetAttribute("src")),
jsReferences);
// Assert: Also contains link tags referencing CSS files
Assert.Equal(
linkElems.Select(tag => tag.GetAttribute("href")),
cssReferences);
}
[Fact]
@ -58,14 +72,12 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
{
// Arrange
var htmlTemplate = "<!DOCTYPE html><html><body><h1 style='color:red'>Hello</h1>Some text<script type='irrelevant'>blah</script></body></html>";
var dependencies = new string[]
{
"System.Abc.dll",
"MyApp.ClassLib.dll",
};
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", dependencies);
htmlTemplate, "MyApp.Entrypoint", "MyNamespace.MyType::MyMethod", assemblyReferences, jsReferences, cssReferences);
// Assert
Assert.Equal(htmlTemplate, content);

View File

@ -199,5 +199,34 @@ namespace Microsoft.AspNetCore.Blazor.E2ETest.Tests
elem => Assert.Equal(typeof(Complex).FullName, elem.Text),
elem => Assert.Equal(typeof(AssemblyHashAlgorithm).FullName, elem.Text));
}
[Fact]
public void CanUseComponentAndStaticContentFromExternalNuGetPackage()
{
var appElement = MountTestComponent<ExternalContentPackage>();
// NuGet packages can use Blazor's JS interop features to provide
// .NET code access to browser APIs
var showPromptButton = appElement.FindElements(By.TagName("button")).First();
showPromptButton.Click();
var modal = Browser.SwitchTo().Alert();
modal.SendKeys("Some value from test");
modal.Accept();
var promptResult = appElement.FindElement(By.TagName("strong"));
Assert.Equal("Some value from test", promptResult.Text);
// NuGet packages can also embed entire Blazor components (themselves
// authored as Razor files), including static content. The CSS value
// here is in a .css file, so if it's correct we know that static content
// file was loaded.
var specialStyleDiv = appElement.FindElement(By.ClassName("special-style"));
Assert.Equal("50px", specialStyleDiv.GetCssValue("padding"));
// The external Blazor components are fully functional, not just static HTML
var externalComponentButton = specialStyleDiv.FindElement(By.TagName("button"));
Assert.Equal("Click me", externalComponentButton.Text);
externalComponentButton.Click();
Assert.Equal("It works", externalComponentButton.Text);
}
}
}

View File

@ -16,4 +16,10 @@
<ProjectReference Include="..\..\..\src\Microsoft.AspNetCore.Blazor\Microsoft.AspNetCore.Blazor.csproj" />
</ItemGroup>
<!-- Local alternative to <PackageReference> to the content package -->
<Import Project="..\TestContentPackage\build\TestContentPackage.props" />
<ItemGroup>
<ProjectReference Include="..\TestContentPackage\TestContentPackage.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,38 @@
@addTagHelper *, TestContentPackage
@using TestContentPackage
<h1>Functionality and content from an external package</h1>
<p>
NuGet packages can embed .NET code, which can in turn call Blazor's
JS interop features if desired. This can be used to distribute new
browser APIs as NuGet packages.
</p>
<p>Click the following button to invoke a JavaScript function.</p>
<button @onclick(ShowJavaScriptPrompt)>Show JavaScript prompt</button>
@if (!string.IsNullOrEmpty(result))
{
<p>Result: <strong>@result</strong></p>
}
<hr />
<p>
Additionally, NuGet packages can contain Blazor components, and even
static resources such as CSS files and images.
</p>
<ComponentFromPackage />
@functions
{
string result;
void ShowJavaScriptPrompt()
{
result = MyPrompt.Show("Hello!");
}
}

View File

@ -23,6 +23,7 @@
<option value="BasicTestApp.HierarchicalImportsTest.Subdir.ComponentUsingImports">Imports statement</option>
<option value="BasicTestApp.HttpClientTest.HttpRequestsComponent">HttpClient tester</option>
<option value="BasicTestApp.BindCasesComponent">@bind cases</option>
<option value="BasicTestApp.ExternalContentPackage">External content package</option>
<!--<option value="BasicTestApp.RouterTest.Default">Router</option> Excluded because it requires additional setup to work correctly when loaded manually -->
</select>
&nbsp;

View File

@ -0,0 +1,15 @@
<div class="special-style">
This component, including the CSS and image required to produce its
elegant styling, is in an external NuGet package.
<button @onclick(ChangeLabel)>@buttonLabel </button>
</div>
@functions
{
string buttonLabel = "Click me";
void ChangeLabel()
{
buttonLabel = "It works";
}
}

View File

@ -0,0 +1,17 @@
using Microsoft.AspNetCore.Blazor.Browser.Interop;
namespace TestContentPackage
{
public static class MyPrompt
{
// Keep in sync with the identifier in the .js file
const string ShowPromptIdentifier = "TestContentPackage.showPrompt";
public static string Show(string message)
{
return RegisteredFunction.Invoke<string>(
ShowPromptIdentifier,
message);
}
}
}

View File

@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<OutputType>library</OutputType>
<IsPackable>true</IsPackable>
</PropertyGroup>
<ItemGroup>
<Content Remove="**" />
<Content Include="build\**" PackagePath="build" />
<Content Include="content\**" PackagePath="content" />
</ItemGroup>
<ItemGroup>
<!-- In real content packages, use a <PackageReference> to Microsoft.AspNetCore.Blazor.Browser instead. -->
<ProjectReference Include="..\..\..\src\Microsoft.AspNetCore.Blazor.Browser\Microsoft.AspNetCore.Blazor.Browser.csproj" />
</ItemGroup>
<!-- In real content packages, use a <PackageReference> to Microsoft.AspNetCore.Blazor.Build instead. -->
<Import Project="..\..\..\src\Microsoft.AspNetCore.Blazor.Build\ReferenceFromSource.props" />
</Project>

View File

@ -0,0 +1,19 @@
<Project>
<PropertyGroup>
<!-- Update this to match your package ID exactly -->
<_PackageId>TestContentPackage</_PackageId>
<_ContentDir>$(MSBuildThisFileDirectory)..\content\</_ContentDir>
</PropertyGroup>
<ItemGroup>
<!-- All files under "content" will be included with the Blazor app build output -->
<BlazorPackageContentFile Include="$(_ContentDir)**" SourcePackage="$(_PackageId)" />
<!-- We'll generate a <script> tag importing each of the following JavaScript files -->
<!-- Change the "Include" pattern if you don't want to include all .js files. -->
<BlazorPackageJsRef Include="$(_ContentDir)**\*.js" SourcePackage="$(_PackageId)" />
<!-- We'll generate a <link> tag importing each of the following CSS files -->
<!-- Change the "Include" pattern if you don't want to include all .css files. -->
<BlazorPackageCssRef Include="$(_ContentDir)**\*.css" SourcePackage="$(_PackageId)" />
</ItemGroup>
</Project>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -0,0 +1,3 @@
Blazor.registerFunction('TestContentPackage.showPrompt', function (message) {
return prompt(message, "Type anything here");
});

View File

@ -0,0 +1,18 @@
.special-style {
background-image: url('./face.png');
padding: 50px;
background-repeat: repeat-x;
border: 5px dashed red;
font-family: "Comic Sans MS";
font-size: 20px;
font-weight: bold;
animation: hideous-rainbow 1s infinite;
}
@keyframes hideous-rainbow {
0% { color: red; }
20% { color: orange; }
40% { color: yellow; }
60% { color: green; }
80% { color: blue; }
}