From 0e9d52fe11b9233fe466486208375fab57bf2040 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Tue, 13 Mar 2018 11:28:23 +0000 Subject: [PATCH] Support components and static content in external NuGet packages (#247) * On build, drop items into dist\_content\(PackageName)\ * Add "); + // 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 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 => diff --git a/src/Microsoft.AspNetCore.Blazor.Build/targets/Blazor.MonoRuntime.props b/src/Microsoft.AspNetCore.Blazor.Build/targets/Blazor.MonoRuntime.props index b2924b901f..545ad3d3ff 100644 --- a/src/Microsoft.AspNetCore.Blazor.Build/targets/Blazor.MonoRuntime.props +++ b/src/Microsoft.AspNetCore.Blazor.Build/targets/Blazor.MonoRuntime.props @@ -11,6 +11,7 @@ -c link -u link -t --verbose + dist/_content/ dist/_framework/ $(BaseBlazorRuntimeOutputPath)_bin/ $(BaseBlazorRuntimeOutputPath)asmjs/ diff --git a/src/Microsoft.AspNetCore.Blazor.Build/targets/Blazor.MonoRuntime.targets b/src/Microsoft.AspNetCore.Blazor.Build/targets/Blazor.MonoRuntime.targets index af1d5c11bb..0b7ebe4696 100644 --- a/src/Microsoft.AspNetCore.Blazor.Build/targets/Blazor.MonoRuntime.targets +++ b/src/Microsoft.AspNetCore.Blazor.Build/targets/Blazor.MonoRuntime.targets @@ -168,6 +168,14 @@ + + <_BlazorPackageContentOutput Include="@(BlazorPackageContentFile)" Condition="%(SourcePackage) != ''"> + $(ProjectDir)$(OutputPath)$(BaseBlazorPackageContentOutputPath)%(SourcePackage)\%(RecursiveDir)\%(Filename)%(Extension) + PreserveNewest + + + + @@ -522,6 +530,8 @@ + + <_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)')" /> - + <_BlazorIndex Include="$(BlazorIndexHtmlOutputPath)" /> diff --git a/test/Microsoft.AspNetCore.Blazor.Build.Test/IndexHtmlWriterTest.cs b/test/Microsoft.AspNetCore.Blazor.Build.Test/IndexHtmlWriterTest.cs index a37c7117d2..8442204332 100644 --- a/test/Microsoft.AspNetCore.Blazor.Build.Test/IndexHtmlWriterTest.cs +++ b/test/Microsoft.AspNetCore.Blazor.Build.Test/IndexHtmlWriterTest.cs @@ -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} {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("" + scriptTagText + ""); - 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 = "

Hello

Some text"; - 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); diff --git a/test/Microsoft.AspNetCore.Blazor.E2ETest/Tests/ComponentRenderingTest.cs b/test/Microsoft.AspNetCore.Blazor.E2ETest/Tests/ComponentRenderingTest.cs index 66006e0773..a3512557b9 100644 --- a/test/Microsoft.AspNetCore.Blazor.E2ETest/Tests/ComponentRenderingTest.cs +++ b/test/Microsoft.AspNetCore.Blazor.E2ETest/Tests/ComponentRenderingTest.cs @@ -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(); + + // 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); + } } } diff --git a/test/testapps/BasicTestApp/BasicTestApp.csproj b/test/testapps/BasicTestApp/BasicTestApp.csproj index 5cb5aa8933..f9e79f9b3d 100644 --- a/test/testapps/BasicTestApp/BasicTestApp.csproj +++ b/test/testapps/BasicTestApp/BasicTestApp.csproj @@ -16,4 +16,10 @@
+ + + + + + diff --git a/test/testapps/BasicTestApp/ExternalContentPackage.cshtml b/test/testapps/BasicTestApp/ExternalContentPackage.cshtml new file mode 100644 index 0000000000..49f86823a5 --- /dev/null +++ b/test/testapps/BasicTestApp/ExternalContentPackage.cshtml @@ -0,0 +1,38 @@ +@addTagHelper *, TestContentPackage +@using TestContentPackage + +

Functionality and content from an external package

+ +

+ 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. +

+ +

Click the following button to invoke a JavaScript function.

+ + + +@if (!string.IsNullOrEmpty(result)) +{ +

Result: @result

+} + +
+ +

+ Additionally, NuGet packages can contain Blazor components, and even + static resources such as CSS files and images. +

+ + + +@functions +{ + string result; + + void ShowJavaScriptPrompt() + { + result = MyPrompt.Show("Hello!"); + } +} diff --git a/test/testapps/BasicTestApp/wwwroot/index.html b/test/testapps/BasicTestApp/wwwroot/index.html index 832c91ebf3..1b7342f7ec 100644 --- a/test/testapps/BasicTestApp/wwwroot/index.html +++ b/test/testapps/BasicTestApp/wwwroot/index.html @@ -23,6 +23,7 @@ +   diff --git a/test/testapps/TestContentPackage/ComponentFromPackage.cshtml b/test/testapps/TestContentPackage/ComponentFromPackage.cshtml new file mode 100644 index 0000000000..a621fcf290 --- /dev/null +++ b/test/testapps/TestContentPackage/ComponentFromPackage.cshtml @@ -0,0 +1,15 @@ +
+ This component, including the CSS and image required to produce its + elegant styling, is in an external NuGet package. + +
+ +@functions +{ + string buttonLabel = "Click me"; + + void ChangeLabel() + { + buttonLabel = "It works"; + } +} diff --git a/test/testapps/TestContentPackage/MyPrompt.cs b/test/testapps/TestContentPackage/MyPrompt.cs new file mode 100644 index 0000000000..44cdd1da0d --- /dev/null +++ b/test/testapps/TestContentPackage/MyPrompt.cs @@ -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( + ShowPromptIdentifier, + message); + } + } +} diff --git a/test/testapps/TestContentPackage/TestContentPackage.csproj b/test/testapps/TestContentPackage/TestContentPackage.csproj new file mode 100644 index 0000000000..bcfa81a9fb --- /dev/null +++ b/test/testapps/TestContentPackage/TestContentPackage.csproj @@ -0,0 +1,23 @@ + + + + netstandard2.0 + library + true + + + + + + + + + + + + + + + + + diff --git a/test/testapps/TestContentPackage/build/TestContentPackage.props b/test/testapps/TestContentPackage/build/TestContentPackage.props new file mode 100644 index 0000000000..fc24a60c2b --- /dev/null +++ b/test/testapps/TestContentPackage/build/TestContentPackage.props @@ -0,0 +1,19 @@ + + + + <_PackageId>TestContentPackage + <_ContentDir>$(MSBuildThisFileDirectory)..\content\ + + + + + + + + + + + + + + diff --git a/test/testapps/TestContentPackage/content/face.png b/test/testapps/TestContentPackage/content/face.png new file mode 100644 index 0000000000000000000000000000000000000000..a96581b73205c21a934050709bc55982036be0d9 GIT binary patch literal 2884 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 i0}^0+VkSVRVE!NCG2R>C7|xUc0000