diff --git a/src/Razor/Microsoft.NET.Sdk.Razor/src/GetDefaultStaticWebAssetsBasePath.cs b/src/Razor/Microsoft.NET.Sdk.Razor/src/GetDefaultStaticWebAssetsBasePath.cs new file mode 100644 index 0000000000..f39f1a789e --- /dev/null +++ b/src/Razor/Microsoft.NET.Sdk.Razor/src/GetDefaultStaticWebAssetsBasePath.cs @@ -0,0 +1,40 @@ +// 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.Build.Framework; +using Microsoft.Build.Utilities; + +namespace Microsoft.AspNetCore.Razor.Tasks +{ + public class GetDefaultStaticWebAssetsBasePath : Task + { + [Required] + public string BasePath { get; set; } + + [Output] + public string SafeBasePath { get; set; } + + public override bool Execute() + { + if (string.IsNullOrWhiteSpace(BasePath)) + { + Log.LogError($"Base path '{BasePath ?? "(null)"}' must contain non-whitespace characters."); + return !Log.HasLoggedErrors; + } + + var safeBasePath = BasePath + .Replace(" ", "") + .Replace(".", "") + .ToLowerInvariant(); + + if (safeBasePath == "") + { + Log.LogError($"Base path '{BasePath}' must contain non '.' characters."); + return !Log.HasLoggedErrors; + } + SafeBasePath = safeBasePath; + + return !Log.HasLoggedErrors; + } + } +} \ No newline at end of file diff --git a/src/Razor/Microsoft.NET.Sdk.Razor/src/build/netstandard2.0/Microsoft.NET.Sdk.Razor.StaticWebAssets.targets b/src/Razor/Microsoft.NET.Sdk.Razor/src/build/netstandard2.0/Microsoft.NET.Sdk.Razor.StaticWebAssets.targets index 0c273df77b..3d403100a6 100644 --- a/src/Razor/Microsoft.NET.Sdk.Razor/src/build/netstandard2.0/Microsoft.NET.Sdk.Razor.StaticWebAssets.targets +++ b/src/Razor/Microsoft.NET.Sdk.Razor/src/build/netstandard2.0/Microsoft.NET.Sdk.Razor.StaticWebAssets.targets @@ -27,17 +27,33 @@ Copyright (c) .NET Foundation. All rights reserved. AssemblyFile="$(RazorSdkBuildTasksAssembly)" Condition="'$(RazorSdkBuildTasksAssembly)' != ''" /> + + ResolveStaticWebAssetsInputs; - _CreateStaticWebAssetsInputsCacheFile + _CreateStaticWebAssetsInputsCacheFile; + $(GenerateStaticWebAssetsManifestDependsOn) + + ResolveStaticWebAssetsInputs; + $(GetCurrentProjectStaticWebAssetsDependsOn) + + GenerateStaticWebAssetsManifest; $(AssignTargetPathsDependsOn) + + _ResolveStaticWebAssetsProjectReferences; + $(ResolveStaticWebAssetsInputsDependsOn) + + @@ -119,34 +135,45 @@ Copyright (c) .NET Foundation. All rights reserved. restores the package and includes the package props file for the package. --> - - - <_SafeBasePath>$(PackageId.Replace('.','')) - + Name="ResolveStaticWebAssetsInputs" + DependsOnTargets="$(ResolveStaticWebAssetsInputsDependsOn)"> + + + + + + + _content/$(_StaticWebAssetSafeBasePath) + + + + <_ThisProjectStaticWebAsset + Include="$(MSBuildProjectDirectory)\wwwroot\**" + Exclude="$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder)" /> - - + + - + $(PackageId) $(MSBuildProjectDirectory)\wwwroot\ - _content\$(_SafeBasePath)\ + $(StaticWebAssetBasePath) %(RecursiveDir)%(FileName)%(Extension) @@ -164,9 +191,57 @@ Copyright (c) .NET Foundation. All rights reserved. - + + + + + + + + + + + + + <_StaticWebAssetsProjectReference Include="%(ReferencePath.MSBuildSourceProjectFile)" /> + + + + + + + + + + <_ThisProjectStaticWebAssets + Include="@(StaticWebAsset)" + Condition="'%(StaticWebAsset.SourceType)' == ''"> + Project + + + + + \ No newline at end of file diff --git a/src/Razor/Microsoft.NET.Sdk.Razor/test/GetDefaultStaticWebAssetsBasePathTest.cs b/src/Razor/Microsoft.NET.Sdk.Razor/test/GetDefaultStaticWebAssetsBasePathTest.cs new file mode 100644 index 0000000000..9a927fc155 --- /dev/null +++ b/src/Razor/Microsoft.NET.Sdk.Razor/test/GetDefaultStaticWebAssetsBasePathTest.cs @@ -0,0 +1,95 @@ +// 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 System.Collections.Generic; +using Microsoft.Build.Framework; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Razor.Tasks +{ + public class GetDefaultStaticWebAssetsBasePathTest + { + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + [InlineData(" ")] + public void ReturnsError_WhenBasePath_DoesNotContainNonWhitespaceCharacters(string basePath) + { + // Arrange + var expectedError = $"Base path '{basePath ?? "(null)"}' must contain non-whitespace characters."; + + var errorMessages = new List(); + var buildEngine = new Mock(); + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + .Callback(args => errorMessages.Add(args.Message)); + + var task = new GetDefaultStaticWebAssetsBasePath + { + BuildEngine = buildEngine.Object, + BasePath = basePath + }; + + // Act + var result = task.Execute(); + + // Assert + Assert.False(result); + var message = Assert.Single(errorMessages); + Assert.Equal(expectedError, message); + } + + [Theory] + [InlineData(".")] + [InlineData("..")] + [InlineData(". ")] + [InlineData(" .")] + [InlineData(" . ")] + [InlineData(". .")] + public void ReturnsError_WhenSafeBasePath_MapsToTheEmptyString(string basePath) + { + // Arrange + var expectedError = $"Base path '{basePath}' must contain non '.' characters."; + + var errorMessages = new List(); + var buildEngine = new Mock(); + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + .Callback(args => errorMessages.Add(args.Message)); + + var task = new GetDefaultStaticWebAssetsBasePath + { + BuildEngine = buildEngine.Object, + BasePath = basePath + }; + + // Act + var result = task.Execute(); + + // Assert + Assert.False(result); + var message = Assert.Single(errorMessages); + Assert.Equal(expectedError, message); + } + + [Theory] + [InlineData("Identity", "identity")] + [InlineData("Microsoft.AspNetCore.Identity", "microsoftaspnetcoreidentity")] + public void ReturnsSafeBasePath_WhenBasePath_ContainsUnsafeCharacters(string basePath, string expectedSafeBasePath) + { + // Arrange + var task = new GetDefaultStaticWebAssetsBasePath + { + BuildEngine = Mock.Of(), + BasePath = basePath + }; + + // Act + var result = task.Execute(); + + // Assert + Assert.True(result); + Assert.Equal(expectedSafeBasePath, task.SafeBasePath); + } + } +} diff --git a/src/Razor/Microsoft.NET.Sdk.Razor/test/IntegrationTests/StaticWebAssetsIntegrationTest.cs b/src/Razor/Microsoft.NET.Sdk.Razor/test/IntegrationTests/StaticWebAssetsIntegrationTest.cs index bb0d250f97..653231d328 100644 --- a/src/Razor/Microsoft.NET.Sdk.Razor/test/IntegrationTests/StaticWebAssetsIntegrationTest.cs +++ b/src/Razor/Microsoft.NET.Sdk.Razor/test/IntegrationTests/StaticWebAssetsIntegrationTest.cs @@ -7,6 +7,7 @@ using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; +using System.Runtime.InteropServices; using System.Threading.Tasks; using Microsoft.Extensions.CommandLineUtils; using Xunit; @@ -32,7 +33,7 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests public ITestOutputHelper Output { get; private set; } [Fact] - [InitializeTestProject("AppWithPackageAndP2PReference")] + [InitializeTestProject("AppWithPackageAndP2PReference",language: "C#", additionalProjects: new[] { "ClassLibrary", "ClassLibrary2" })] public async Task Build_GeneratesStaticWebAssetsManifest_Success_CreatesManifest() { var result = await DotnetMSBuild("Build", "/restore"); @@ -71,7 +72,7 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests } [Fact] - [InitializeTestProject("AppWithPackageAndP2PReference")] + [InitializeTestProject("AppWithPackageAndP2PReference",language: "C#", additionalProjects: new[] { "ClassLibrary", "ClassLibrary2" })] public async Task Clean_Success_RemovesManifestAndCache() { var result = await DotnetMSBuild("Build", "/restore"); @@ -92,7 +93,7 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests } [Fact] - [InitializeTestProject("AppWithPackageAndP2PReference")] + [InitializeTestProject("AppWithPackageAndP2PReference",language: "C#", additionalProjects: new[] { "ClassLibrary", "ClassLibrary2" })] public async Task Rebuild_Success_RecreatesManifestAndCache() { // Arrange @@ -142,7 +143,7 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests } [Fact] - [InitializeTestProject("AppWithPackageAndP2PReference")] + [InitializeTestProject("AppWithPackageAndP2PReference",language: "C#", additionalProjects: new[] { "ClassLibrary", "ClassLibrary2" })] public async Task GenerateStaticWebAssetsManifest_IncrementalBuild_ReusesManifest() { var result = await DotnetMSBuild("GenerateStaticWebAssetsManifest", "/restore"); @@ -192,16 +193,25 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests private string GetExpectedManifest() { + // We need to do this for Mac as apparently the temp folder in mac is prepended by /private by the os, even though the current user + // can refer to it without the /private prefix. We don't care a lot about the specific path in this test as we will have tests that + // validate the behavior at runtime. + var source = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? $"/private{Project.SolutionPath}" : Project.SolutionPath; + var restorePath = LocalNugetPackagesCacheTempPath; var projects = new[] { Path.Combine(restorePath, "packagelibrarytransitivedependency", "1.0.0", "buildTransitive", "..", "razorContent") + Path.DirectorySeparatorChar, - Path.Combine(restorePath, "packagelibrarydirectdependency", "1.0.0", "build", "..", "razorContent") + Path.DirectorySeparatorChar + Path.Combine(restorePath, "packagelibrarydirectdependency", "1.0.0", "build", "..", "razorContent") + Path.DirectorySeparatorChar, + Path.GetFullPath(Path.Combine(source, "ClassLibrary", "wwwroot")) + Path.DirectorySeparatorChar, + Path.GetFullPath(Path.Combine(source, "ClassLibrary2", "wwwroot")) + Path.DirectorySeparatorChar }; return $@" + + "; } } diff --git a/src/Razor/test/testassets/AppWithPackageAndP2PReference/AppWithPackageAndP2PReference.csproj b/src/Razor/test/testassets/AppWithPackageAndP2PReference/AppWithPackageAndP2PReference.csproj index 4eb52fbfa3..076d9b77b0 100644 --- a/src/Razor/test/testassets/AppWithPackageAndP2PReference/AppWithPackageAndP2PReference.csproj +++ b/src/Razor/test/testassets/AppWithPackageAndP2PReference/AppWithPackageAndP2PReference.csproj @@ -11,7 +11,6 @@ $(RestoreSources); $(RuntimeAdditionalRestoreSources) - @@ -30,7 +29,11 @@ - + + + + + $(RepositoryRoot)artifacts\bin\Microsoft.AspNetCore.Razor.Test.MvcShim.ClassLib\$(Configuration)\netstandard2.0\ diff --git a/src/Razor/test/testassets/ClassLibrary/wwwroot/js/project-transitive-dep.js b/src/Razor/test/testassets/ClassLibrary/wwwroot/js/project-transitive-dep.js new file mode 100644 index 0000000000..c8ee8c4b87 --- /dev/null +++ b/src/Razor/test/testassets/ClassLibrary/wwwroot/js/project-transitive-dep.js @@ -0,0 +1,3 @@ +(function () { + document.getElementById('project-transitive-dep').innerHTML = 'project-transitive-dep'; +})() \ No newline at end of file diff --git a/src/Razor/test/testassets/ClassLibrary/wwwroot/js/project-transitive-dep.v4.js b/src/Razor/test/testassets/ClassLibrary/wwwroot/js/project-transitive-dep.v4.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/Razor/test/testassets/ClassLibrary2/wwwroot/css/site.css b/src/Razor/test/testassets/ClassLibrary2/wwwroot/css/site.css new file mode 100644 index 0000000000..c9d72143d2 --- /dev/null +++ b/src/Razor/test/testassets/ClassLibrary2/wwwroot/css/site.css @@ -0,0 +1 @@ +div.fluent { display: inline-block } \ No newline at end of file diff --git a/src/Razor/test/testassets/ClassLibrary2/wwwroot/js/project-direct-dep.js b/src/Razor/test/testassets/ClassLibrary2/wwwroot/js/project-direct-dep.js new file mode 100644 index 0000000000..f6d99d4251 --- /dev/null +++ b/src/Razor/test/testassets/ClassLibrary2/wwwroot/js/project-direct-dep.js @@ -0,0 +1,3 @@ +(function () { + document.getElementById('project-direct-dep').innerHTML = 'project-direct-dep'; +})() \ No newline at end of file