Skip to content
This repository has been archived by the owner on Feb 25, 2021. It is now read-only.

Commit

Permalink
Support components and static content in external NuGet packages (#247)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
SteveSandersonMS committed Mar 14, 2018
1 parent 6eadc43 commit 5ec0292
Show file tree
Hide file tree
Showing 17 changed files with 265 additions and 19 deletions.
11 changes: 11 additions & 0 deletions Blazor.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@ public static void Command(CommandLineApplication command)
"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);
Expand All @@ -36,7 +44,13 @@ public static void Command(CommandLineApplication command)
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)
Expand Down
35 changes: 31 additions & 4 deletions src/Microsoft.AspNetCore.Blazor.Build/Core/IndexHtmlWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -24,7 +30,7 @@ public static void UpdateIndex(string path, string assemblyPath, IEnumerable<str
}
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);
Expand Down Expand Up @@ -93,7 +99,9 @@ public static string GetIndexHtmlContents(
string htmlTemplate,
string assemblyName,
string assemblyEntryPoint,
IEnumerable<string> binFiles)
IEnumerable<string> assemblyReferences,
IEnumerable<string> jsReferences,
IEnumerable<string> cssReferences)
{
var resultBuilder = new StringBuilder();

Expand Down Expand Up @@ -132,9 +140,19 @@ public static string GetIndexHtmlContents(
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;
Expand All @@ -160,6 +178,15 @@ public static string GetIndexHtmlContents(
}
}

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 =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 -->
Expand Down Expand Up @@ -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
Expand All @@ -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)" />
Expand Down
38 changes: 25 additions & 13 deletions test/Microsoft.AspNetCore.Blazor.Build.Test/IndexHtmlWriterTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -25,15 +26,16 @@ Some text
$@"{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);
Expand All @@ -42,7 +44,9 @@ Some text
// 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"));
Expand All @@ -51,21 +55,29 @@ Some text
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]
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 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -199,5 +199,34 @@ public void CanUseViewImportsHierarchically()
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);
}
}
}
6 changes: 6 additions & 0 deletions test/testapps/BasicTestApp/BasicTestApp.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -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>
38 changes: 38 additions & 0 deletions test/testapps/BasicTestApp/ExternalContentPackage.cshtml
Original file line number Diff line number Diff line change
@@ -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!");
}
}
1 change: 1 addition & 0 deletions test/testapps/BasicTestApp/wwwroot/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
15 changes: 15 additions & 0 deletions test/testapps/TestContentPackage/ComponentFromPackage.cshtml
Original file line number Diff line number Diff line change
@@ -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";
}
}
17 changes: 17 additions & 0 deletions test/testapps/TestContentPackage/MyPrompt.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
Loading

0 comments on commit 5ec0292

Please sign in to comment.