diff --git a/Blazor.sln b/Blazor.sln index c1c5691370..6fc39fe6ca 100644 --- a/Blazor.sln +++ b/Blazor.sln @@ -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} diff --git a/src/Microsoft.AspNetCore.Blazor.Build/Cli/Commands/BuildIndexHtmlCommand.cs b/src/Microsoft.AspNetCore.Blazor.Build/Cli/Commands/BuildIndexHtmlCommand.cs index 02e188c1cb..d689c59ffb 100644 --- a/src/Microsoft.AspNetCore.Blazor.Build/Cli/Commands/BuildIndexHtmlCommand.cs +++ b/src/Microsoft.AspNetCore.Blazor.Build/Cli/Commands/BuildIndexHtmlCommand.cs @@ -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 "); + // 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 0000000000..a96581b732 Binary files /dev/null and b/test/testapps/TestContentPackage/content/face.png differ diff --git a/test/testapps/TestContentPackage/content/prompt.js b/test/testapps/TestContentPackage/content/prompt.js new file mode 100644 index 0000000000..e5b0e24d50 --- /dev/null +++ b/test/testapps/TestContentPackage/content/prompt.js @@ -0,0 +1,3 @@ +Blazor.registerFunction('TestContentPackage.showPrompt', function (message) { + return prompt(message, "Type anything here"); +}); diff --git a/test/testapps/TestContentPackage/content/styles.css b/test/testapps/TestContentPackage/content/styles.css new file mode 100644 index 0000000000..95137fe983 --- /dev/null +++ b/test/testapps/TestContentPackage/content/styles.css @@ -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; } +}