From 6ec0c445ceaaaef41aac29c36478750c29ad5ae1 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Wed, 27 Nov 2019 15:48:58 -0800 Subject: [PATCH 1/4] Load the netcoreapp3.1 compiled task when running in Core MSBuild Fixes https://github.com/aspnet/AspNetCore/issues/17308 \n\nCommit migrated from https://github.com/dotnet/aspnetcore-tooling/commit/51e2a0ee646d3a161af4d88340419343286eba48 --- .../Sdk.Razor.CurrentVersion.targets | 2 +- .../BuildIntrospectionTest.cs | 47 +++++++++++++++++++ .../test/Microsoft.NET.Sdk.Razor.Test.csproj | 5 ++ .../RazorTest.Introspection.targets | 7 +++ 4 files changed, 60 insertions(+), 1 deletion(-) diff --git a/src/Razor/Microsoft.NET.Sdk.Razor/src/build/netstandard2.0/Sdk.Razor.CurrentVersion.targets b/src/Razor/Microsoft.NET.Sdk.Razor/src/build/netstandard2.0/Sdk.Razor.CurrentVersion.targets index 84d7bb730c..e5e8d926a0 100644 --- a/src/Razor/Microsoft.NET.Sdk.Razor/src/build/netstandard2.0/Sdk.Razor.CurrentVersion.targets +++ b/src/Razor/Microsoft.NET.Sdk.Razor/src/build/netstandard2.0/Sdk.Razor.CurrentVersion.targets @@ -30,7 +30,7 @@ Copyright (c) .NET Foundation. All rights reserved. $(MSBuildThisFileDirectory)..\..\ $(RazorSdkDirectoryRoot)tasks\ - <_RazorSdkTasksTFM Condition=" '$(MSBuildRuntimeType)' == 'Core'">$(DefaultNetCoreTargetFramework) + <_RazorSdkTasksTFM Condition=" '$(MSBuildRuntimeType)' == 'Core'">netcoreapp3.1 <_RazorSdkTasksTFM Condition=" '$(_RazorSdkTasksTFM)' == ''">net46 $(RazorSdkBuildTasksDirectoryRoot)$(_RazorSdkTasksTFM)\Microsoft.NET.Sdk.Razor.Tasks.dll diff --git a/src/Razor/Microsoft.NET.Sdk.Razor/test/IntegrationTests/BuildIntrospectionTest.cs b/src/Razor/Microsoft.NET.Sdk.Razor/test/IntegrationTests/BuildIntrospectionTest.cs index fcb33acbf2..acff14237e 100644 --- a/src/Razor/Microsoft.NET.Sdk.Razor/test/IntegrationTests/BuildIntrospectionTest.cs +++ b/src/Razor/Microsoft.NET.Sdk.Razor/test/IntegrationTests/BuildIntrospectionTest.cs @@ -2,7 +2,10 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.IO; +using System.Linq; +using System.Reflection; using System.Threading.Tasks; +using Microsoft.AspNetCore.Testing; using Xunit; namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests @@ -221,5 +224,49 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests Assert.BuildOutputContainsLine(result, "Content: appsettings.json CopyToOutputDirectory= CopyToPublishDirectory=PreserveNewest ExcludeFromSingleFile=true"); Assert.BuildOutputContainsLine(result, "Content: appsettings.Development.json CopyToOutputDirectory= CopyToPublishDirectory=PreserveNewest ExcludeFromSingleFile=true"); } + + [Fact] + [InitializeTestProject("SimpleMvc")] + public async Task IntrospectRazorTasksDllPath() + { + // Regression test for https://github.com/aspnet/AspNetCore/issues/17308 + var solutionRoot = GetType().Assembly.GetCustomAttributes() + .First(a => a.Key == "Testing.RepoRoot") + .Value; + + var tfm = +#if NETCOREAPP3_1 + "netcoreapp3.1"; +#else +#error Target framework needs to be updated. +#endif + + var expected = Path.Combine(solutionRoot, "artifacts", "bin", "Microsoft.NET.Sdk.Razor", Configuration, "sdk-output", "tasks", tfm, "Microsoft.NET.Sdk.Razor.Tasks.dll"); + + // Verifies the fix for https://github.com/aspnet/AspNetCore/issues/17308 + var result = await DotnetMSBuild("_IntrospectRazorTasks"); + + Assert.BuildPassed(result); + Assert.BuildOutputContainsLine(result, $"RazorTasksPath: {expected}"); + } + + [ConditionalFact] + [OSSkipCondition(OperatingSystems.Linux | OperatingSystems.MacOSX)] + [InitializeTestProject("SimpleMvc")] + public async Task IntrospectRazorTasksDllPath_DesktopMsBuild() + { + var solutionRoot = GetType().Assembly.GetCustomAttributes() + .First(a => a.Key == "Testing.RepoRoot") + .Value; + + var tfm = "net46"; + var expected = Path.Combine(solutionRoot, "artifacts", "bin", "Microsoft.NET.Sdk.Razor", Configuration, "sdk-output", "tasks", tfm, "Microsoft.NET.Sdk.Razor.Tasks.dll"); + + // Verifies the fix for https://github.com/aspnet/AspNetCore/issues/17308 + var result = await DotnetMSBuild("_IntrospectRazorTasks", msBuildProcessKind: MSBuildProcessKind.Desktop); + + Assert.BuildPassed(result); + Assert.BuildOutputContainsLine(result, $"RazorTasksPath: {expected}"); + } } } diff --git a/src/Razor/Microsoft.NET.Sdk.Razor/test/Microsoft.NET.Sdk.Razor.Test.csproj b/src/Razor/Microsoft.NET.Sdk.Razor/test/Microsoft.NET.Sdk.Razor.Test.csproj index 0efaf7b8fd..e52fbfb440 100644 --- a/src/Razor/Microsoft.NET.Sdk.Razor/test/Microsoft.NET.Sdk.Razor.Test.csproj +++ b/src/Razor/Microsoft.NET.Sdk.Razor/test/Microsoft.NET.Sdk.Razor.Test.csproj @@ -53,6 +53,11 @@ <_Parameter2>$(ProcDumpPath) + + <_Parameter1>Testing.RepoRoot + <_Parameter2>$(RepoRoot) + + diff --git a/src/Razor/test/testassets/RazorTest.Introspection.targets b/src/Razor/test/testassets/RazorTest.Introspection.targets index c3fca78317..de0fc9ee3e 100644 --- a/src/Razor/test/testassets/RazorTest.Introspection.targets +++ b/src/Razor/test/testassets/RazorTest.Introspection.targets @@ -46,4 +46,11 @@ + + + + <_SdkTaskPath>$([System.IO.Path]::GetFullPath('$(RazorSdkBuildTasksAssembly)')) + + + From 0c907a8225664907afdd5e35afe7b3f603b2b3d0 Mon Sep 17 00:00:00 2001 From: Javier Calvarro Nelson Date: Thu, 5 Dec 2019 05:50:03 -0800 Subject: [PATCH 2/4] [Static web assets][Fixes #AspNetCore/17079] PublishSingleFile results in 404 errors for Nuget components \n\nCommit migrated from https://github.com/dotnet/aspnetcore-tooling/commit/53dd89a41d4c3fa9553636746e7074236e583a99 --- ...soft.NET.Sdk.Razor.StaticWebAssets.targets | 5 ++-- .../MSBuildIntegrationTestBase.cs | 2 ++ .../StaticWebAssetsIntegrationTest.cs | 25 +++++++++++++++++++ 3 files changed, 30 insertions(+), 2 deletions(-) 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 a200ec63ab..693b0d87dd 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 @@ -506,14 +506,15 @@ Copyright (c) .NET Foundation. All rights reserved. PreserveNewest $([MSBuild]::MakeRelative('$(MSBuildProjectDirectory)','$([MSBuild]::NormalizePath('wwwroot\%(BasePath)\%(RelativePath)'))')) - - + + true + diff --git a/src/Razor/Microsoft.NET.Sdk.Razor/test/IntegrationTests/MSBuildIntegrationTestBase.cs b/src/Razor/Microsoft.NET.Sdk.Razor/test/IntegrationTests/MSBuildIntegrationTestBase.cs index 34c4b87961..ddc33fb6f0 100644 --- a/src/Razor/Microsoft.NET.Sdk.Razor/test/IntegrationTests/MSBuildIntegrationTestBase.cs +++ b/src/Razor/Microsoft.NET.Sdk.Razor/test/IntegrationTests/MSBuildIntegrationTestBase.cs @@ -40,6 +40,8 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests protected string PublishOutputPath => Path.Combine(OutputPath, "publish"); + protected string GetRidSpecificPublishOutputPath(string rid) => Path.Combine(OutputPath, rid, "publish"); + // Used by the test framework to set the project that we're working with internal static ProjectDirectory Project { 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 bc8b17d705..fa747f98b3 100644 --- a/src/Razor/Microsoft.NET.Sdk.Razor/test/IntegrationTests/StaticWebAssetsIntegrationTest.cs +++ b/src/Razor/Microsoft.NET.Sdk.Razor/test/IntegrationTests/StaticWebAssetsIntegrationTest.cs @@ -83,6 +83,31 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests Assert.FileDoesNotExist(result, PublishOutputPath, "AppWithPackageAndP2PReference.StaticWebAssets.xml"); } + [Fact] + [InitializeTestProject("AppWithPackageAndP2PReference", additionalProjects: new[] { "ClassLibrary", "ClassLibrary2" })] + public async Task Publish_CopiesStaticWebAssetsToDestinationFolder_PublishSingleFile() + { + var runtimeIdentifier = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "win-x64" : (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "osx-x64" : "linux-x64"); + var result = await DotnetMSBuild("Publish", $"/restore /p:PublishSingleFile=true /p:RuntimeIdentifier={runtimeIdentifier}"); + + Assert.BuildPassed(result); + var publishOutputPath = GetRidSpecificPublishOutputPath(runtimeIdentifier); + Assert.FileExists(result, publishOutputPath, Path.Combine("wwwroot", "_content", "ClassLibrary", "js", "project-transitive-dep.js")); + Assert.FileExists(result, publishOutputPath, Path.Combine("wwwroot", "_content", "ClassLibrary", "js", "project-transitive-dep.v4.js")); + Assert.FileExists(result, publishOutputPath, Path.Combine("wwwroot", "_content", "ClassLibrary2", "css", "site.css")); + Assert.FileExists(result, publishOutputPath, Path.Combine("wwwroot", "_content", "ClassLibrary2", "js", "project-direct-dep.js")); + Assert.FileExists(result, publishOutputPath, Path.Combine("wwwroot", "_content", "PackageLibraryDirectDependency", "css", "site.css")); + Assert.FileExists(result, publishOutputPath, Path.Combine("wwwroot", "_content", "PackageLibraryDirectDependency", "js", "pkg-direct-dep.js")); + Assert.FileExists(result, publishOutputPath, Path.Combine("wwwroot", "_content", "PackageLibraryTransitiveDependency", "js", "pkg-transitive-dep.js")); + + // Validate that static web assets don't get published as content too on their regular path + Assert.FileDoesNotExist(result, publishOutputPath, Path.Combine("wwwroot", "js", "project-transitive-dep.js")); + Assert.FileDoesNotExist(result, publishOutputPath, Path.Combine("wwwroot", "js", "project-transitive-dep.v4.js")); + + // Validate that the manifest never gets copied + Assert.FileDoesNotExist(result, publishOutputPath, "AppWithPackageAndP2PReference.StaticWebAssets.xml"); + } + [Fact] [InitializeTestProject("AppWithPackageAndP2PReference", additionalProjects: new[] { "ClassLibrary", "ClassLibrary2" })] public async Task Publish_WithBuildReferencesDisabled_CopiesStaticWebAssetsToDestinationFolder() From 3ff767b3d3d66a5fa740e556df07c3278ceb0c46 Mon Sep 17 00:00:00 2001 From: Javier Calvarro Nelson Date: Tue, 10 Dec 2019 10:17:40 -0800 Subject: [PATCH 3/4] [StaticWebAssets][Fixes #AspNetCore/17426] Publish no build doesn't copy static web assets from referenced projects \n\nCommit migrated from https://github.com/dotnet/aspnetcore-tooling/commit/6685cd61050a0f010d68ba118b2570693a3d9b8f --- ...soft.NET.Sdk.Razor.StaticWebAssets.targets | 20 ++++++++----------- .../StaticWebAssetsIntegrationTest.cs | 20 +++++++++++++++++++ 2 files changed, 28 insertions(+), 12 deletions(-) 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 a200ec63ab..1ed75f2965 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 @@ -54,23 +54,19 @@ Copyright (c) .NET Foundation. All rights reserved. $(GetCurrentProjectStaticWebAssetsDependsOn) - + + $(GetCopyToOutputDirectoryItemsDependsOn); GenerateStaticWebAssetsManifest; - $(AssignTargetPathsDependsOn) - + ResolveCurrentProjectStaticWebAssetsInputs; - $(ResolveStaticWebAssetsInputsDependsOn) - - - ResolveReferencedProjectsStaticWebAssets; $(ResolveStaticWebAssetsInputsDependsOn) - ResolveReferences; + PrepareProjectReferences; $(ResolveReferencedProjectsStaticWebAssetsDependsOn) @@ -187,14 +183,14 @@ Copyright (c) .NET Foundation. All rights reserved. - + Condition="'@(_ExternalStaticWebAsset->Count())' != '0'"> + $(TargetName).StaticWebAssets.xml PreserveNewest Never - + 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 bc8b17d705..3120da0be1 100644 --- a/src/Razor/Microsoft.NET.Sdk.Razor/test/IntegrationTests/StaticWebAssetsIntegrationTest.cs +++ b/src/Razor/Microsoft.NET.Sdk.Razor/test/IntegrationTests/StaticWebAssetsIntegrationTest.cs @@ -104,6 +104,26 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests Assert.FileExists(publish, PublishOutputPath, Path.Combine("wwwroot", "_content", "PackageLibraryTransitiveDependency", "js", "pkg-transitive-dep.js")); } + [Fact] + [InitializeTestProject("AppWithPackageAndP2PReference", additionalProjects: new[] { "ClassLibrary", "ClassLibrary2" })] + public async Task Publish_NoBuild_CopiesStaticWebAssetsToDestinationFolder() + { + var build = await DotnetMSBuild("Build", "/restore"); + + Assert.BuildPassed(build); + + var publish = await DotnetMSBuild("Publish", "/p:NoBuild=true"); + + Assert.BuildPassed(publish); + + Assert.FileExists(publish, PublishOutputPath, Path.Combine("wwwroot", "_content", "ClassLibrary", "js", "project-transitive-dep.js")); + Assert.FileExists(publish, PublishOutputPath, Path.Combine("wwwroot", "_content", "ClassLibrary", "js", "project-transitive-dep.v4.js")); + Assert.FileExists(publish, PublishOutputPath, Path.Combine("wwwroot", "_content", "ClassLibrary2", "css", "site.css")); + Assert.FileExists(publish, PublishOutputPath, Path.Combine("wwwroot", "_content", "ClassLibrary2", "js", "project-direct-dep.js")); + Assert.FileExists(publish, PublishOutputPath, Path.Combine("wwwroot", "_content", "PackageLibraryDirectDependency", "css", "site.css")); + Assert.FileExists(publish, PublishOutputPath, Path.Combine("wwwroot", "_content", "PackageLibraryDirectDependency", "js", "pkg-direct-dep.js")); + Assert.FileExists(publish, PublishOutputPath, Path.Combine("wwwroot", "_content", "PackageLibraryTransitiveDependency", "js", "pkg-transitive-dep.js")); + } [Fact] [InitializeTestProject("SimpleMvc")] From 541323631fc1b2662f3fbdc83154581c2f9e2c0d Mon Sep 17 00:00:00 2001 From: Javier Calvarro Nelson Date: Tue, 26 Nov 2019 04:52:46 -0800 Subject: [PATCH 4/4] [StaticWebAssets] Updates manifest generation to allow multiple content roots under the same base path for a given project. \n\nCommit migrated from https://github.com/dotnet/aspnetcore-tooling/commit/5e68cb88e4602aa5c814325abee7334265c55d33 --- .../src/GenerateStaticWebAssetsManifest.cs | 36 ++++- .../src/ValidateStaticWebAssetsUniquePaths.cs | 86 +++++++++++ ...soft.NET.Sdk.Razor.StaticWebAssets.targets | 19 ++- ...erateAspNetCoreStaticAssetsManifestTest.cs | 69 ++++++++- .../ValidateStaticWebAssetsUniquePathsTest.cs | 141 ++++++++++++++++++ 5 files changed, 336 insertions(+), 15 deletions(-) create mode 100644 src/Razor/Microsoft.NET.Sdk.Razor/src/ValidateStaticWebAssetsUniquePaths.cs create mode 100644 src/Razor/Microsoft.NET.Sdk.Razor/test/ValidateStaticWebAssetsUniquePathsTest.cs diff --git a/src/Razor/Microsoft.NET.Sdk.Razor/src/GenerateStaticWebAssetsManifest.cs b/src/Razor/Microsoft.NET.Sdk.Razor/src/GenerateStaticWebAssetsManifest.cs index 3ab75b0452..b8d5d717df 100644 --- a/src/Razor/Microsoft.NET.Sdk.Razor/src/GenerateStaticWebAssetsManifest.cs +++ b/src/Razor/Microsoft.NET.Sdk.Razor/src/GenerateStaticWebAssetsManifest.cs @@ -17,6 +17,7 @@ namespace Microsoft.AspNetCore.Razor.Tasks { private const string ContentRoot = "ContentRoot"; private const string BasePath = "BasePath"; + private const string SourceId = "SourceId"; [Required] public string TargetManifestPath { get; set; } @@ -76,13 +77,19 @@ namespace Microsoft.AspNetCore.Razor.Tasks // so it needs to always be '/'. var normalizedBasePath = basePath.Replace("\\", "/"); + // contentRoot can have forward and trailing slashes and sometimes consecutive directory + // separators. To be more flexible we will normalize the content root so that it contains a + // single trailing separator. + var normalizedContentRoot = $"{contentRoot.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)}{Path.DirectorySeparatorChar}"; + // At this point we already know that there are no elements with different base paths and same content roots // or viceversa. Here we simply skip additional items that have the same base path and same content root. - if (!nodes.Exists(e => e.Attribute(BasePath).Value.Equals(normalizedBasePath, StringComparison.OrdinalIgnoreCase))) + if (!nodes.Exists(e => e.Attribute("BasePath").Value.Equals(normalizedBasePath, StringComparison.OrdinalIgnoreCase) && + e.Attribute("Path").Value.Equals(normalizedContentRoot, StringComparison.OrdinalIgnoreCase))) { nodes.Add(new XElement("ContentRoot", new XAttribute("BasePath", normalizedBasePath), - new XAttribute("Path", contentRoot))); + new XAttribute("Path", normalizedContentRoot))); } } @@ -102,7 +109,8 @@ namespace Microsoft.AspNetCore.Razor.Tasks { var contentRootDefinition = ContentRootDefinitions[i]; if (!EnsureRequiredMetadata(contentRootDefinition, BasePath) || - !EnsureRequiredMetadata(contentRootDefinition, ContentRoot)) + !EnsureRequiredMetadata(contentRootDefinition, ContentRoot) || + !EnsureRequiredMetadata(contentRootDefinition, SourceId)) { return false; } @@ -126,23 +134,35 @@ namespace Microsoft.AspNetCore.Razor.Tasks var contentRootDefinition = ContentRootDefinitions[i]; var basePath = contentRootDefinition.GetMetadata(BasePath); var contentRoot = contentRootDefinition.GetMetadata(ContentRoot); + var sourceId = contentRootDefinition.GetMetadata(SourceId); if (basePaths.TryGetValue(basePath, out var existingBasePath)) { var existingBasePathContentRoot = existingBasePath.GetMetadata(ContentRoot); - if (!string.Equals(contentRoot, existingBasePathContentRoot, StringComparison.OrdinalIgnoreCase)) + var existingSourceId = existingBasePath.GetMetadata(SourceId); + if (!string.Equals(contentRoot, existingBasePathContentRoot, StringComparison.OrdinalIgnoreCase) && + // We want to check this case to allow for client-side blazor projects to have multiple different content + // root sources exposed under the same base path while still requiring unique base paths/content roots across + // project/package boundaries. + !string.Equals(sourceId, existingSourceId, StringComparison.OrdinalIgnoreCase)) { // Case: - // Item1: /_content/Library, /package/aspnetContent1 - // Item2: /_content/Library, /package/aspnetContent2 + // Item2: /_content/Library, project:/project/aspnetContent2 + // Item1: /_content/Library, package:/package/aspnetContent1 Log.LogError($"Duplicate base paths '{basePath}' for content root paths '{contentRoot}' and '{existingBasePathContentRoot}'. " + $"('{contentRootDefinition.ItemSpec}', '{existingBasePath.ItemSpec}')"); return false; } + // It was a duplicate, so we skip it. // Case: - // Item1: /_content/Library, /package/aspnetContent - // Item2: /_content/Library, /package/aspnetContent + // Item1: /_content/Library, project:/project/aspnetContent + // Item2: /_content/Library, project:/project/aspnetContent + + // It was a separate content root exposed from the same project/package, so we skip it. + // Case: + // Item1: /_content/Library, project:/project/aspnetContent/bin/debug/netstandard2.1/dist + // Item2: /_content/Library, project:/project/wwwroot } else { diff --git a/src/Razor/Microsoft.NET.Sdk.Razor/src/ValidateStaticWebAssetsUniquePaths.cs b/src/Razor/Microsoft.NET.Sdk.Razor/src/ValidateStaticWebAssetsUniquePaths.cs new file mode 100644 index 0000000000..23ec446549 --- /dev/null +++ b/src/Razor/Microsoft.NET.Sdk.Razor/src/ValidateStaticWebAssetsUniquePaths.cs @@ -0,0 +1,86 @@ +// 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; +using System.Collections.Generic; +using System.IO; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +namespace Microsoft.AspNetCore.Razor.Tasks +{ + public class ValidateStaticWebAssetsUniquePaths : Task + { + private const string BasePath = "BasePath"; + private const string RelativePath = "RelativePath"; + private const string TargetPath = "TargetPath"; + + [Required] + public ITaskItem[] StaticWebAssets { get; set; } + + [Required] + public ITaskItem[] WebRootFiles { get; set; } + + public override bool Execute() + { + var assetsByWebRootPaths = new Dictionary(StringComparer.OrdinalIgnoreCase); + for (var i = 0; i < StaticWebAssets.Length; i++) + { + var contentRootDefinition = StaticWebAssets[i]; + if (!EnsureRequiredMetadata(contentRootDefinition, BasePath) || + !EnsureRequiredMetadata(contentRootDefinition, RelativePath)) + { + return false; + } + else + { + var webRootPath = GetWebRootPath( + contentRootDefinition.GetMetadata(BasePath), + contentRootDefinition.GetMetadata(RelativePath)); + + if (assetsByWebRootPaths.TryGetValue(webRootPath, out var existingWebRootPath)) + { + if (!string.Equals(contentRootDefinition.ItemSpec, existingWebRootPath.ItemSpec, StringComparison.OrdinalIgnoreCase)) + { + Log.LogError($"Conflicting assets with the same path '{webRootPath}' for content root paths '{contentRootDefinition.ItemSpec}' and '{existingWebRootPath.ItemSpec}'."); + return false; + } + } + else + { + assetsByWebRootPaths.Add(webRootPath, contentRootDefinition); + } + } + } + + for (var i = 0; i < WebRootFiles.Length; i++) + { + var webRootFile = WebRootFiles[i]; + var relativePath = webRootFile.GetMetadata(TargetPath); + var webRootFileWebRootPath = GetWebRootPath("/", relativePath); + if (assetsByWebRootPaths.TryGetValue(webRootFileWebRootPath, out var existingAsset)) + { + Log.LogError($"The static web asset '{existingAsset.ItemSpec}' has a conflicting web root path '{webRootFileWebRootPath}' with the project file '{webRootFile.ItemSpec}'."); + return false; + } + } + + return true; + } + + // Normalizes /base/relative \base\relative\ base\relative and so on to /base/relative + private string GetWebRootPath(string basePath, string relativePath) => $"/{Path.Combine(basePath, relativePath.TrimStart('.').TrimStart('/')).Replace("\\", "/").Trim('/')}"; + + private bool EnsureRequiredMetadata(ITaskItem item, string metadataName) + { + var value = item.GetMetadata(metadataName); + if (string.IsNullOrEmpty(value)) + { + Log.LogError($"Missing required metadata '{metadataName}' for '{item.ItemSpec}'."); + return false; + } + + return true; + } + } +} 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 a200ec63ab..d233f8682f 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 @@ -32,6 +32,11 @@ Copyright (c) .NET Foundation. All rights reserved. AssemblyFile="$(RazorSdkBuildTasksAssembly)" Condition="'$(RazorSdkBuildTasksAssembly)' != ''" /> + + %(StaticWebAsset.BasePath) %(StaticWebAsset.ContentRoot) + %(StaticWebAsset.SourceId) - + @@ -181,6 +187,15 @@ Copyright (c) .NET Foundation. All rights reserved. Outputs="$(_GeneratedStaticWebAssetsDevelopmentManifest)" DependsOnTargets="$(GenerateStaticWebAssetsManifestDependsOn)"> + + <_WebRootFiles Include="@(ContentWithTargetPath)" Condition="$([System.String]::Copy('%(TargetPath)').Replace('\','/').StartsWith('wwwroot/'))" /> + <_ReferencedStaticWebAssets Include="@(StaticWebAsset)" Condition="'%(SourceType)' != ''" /> + + + + @@ -273,7 +288,7 @@ Copyright (c) .NET Foundation. All rights reserved. <_ThisProjectStaticWebAsset Include="@(Content)" - Condition="$([System.String]::Copy('%(Identity)').StartsWith('wwwroot'))"> + Condition="$([System.String]::Copy('%(Identity)').Replace('\','/').StartsWith('wwwroot/'))"> $([System.String]::Copy('%(Identity)').Substring(8)) diff --git a/src/Razor/Microsoft.NET.Sdk.Razor/test/GenerateAspNetCoreStaticAssetsManifestTest.cs b/src/Razor/Microsoft.NET.Sdk.Razor/test/GenerateAspNetCoreStaticAssetsManifestTest.cs index 8bb80e5c2c..bc89a9b185 100644 --- a/src/Razor/Microsoft.NET.Sdk.Razor/test/GenerateAspNetCoreStaticAssetsManifestTest.cs +++ b/src/Razor/Microsoft.NET.Sdk.Razor/test/GenerateAspNetCoreStaticAssetsManifestTest.cs @@ -89,12 +89,14 @@ namespace Microsoft.AspNetCore.Razor.Tasks CreateItem(Path.Combine("wwwroot","sample.js"), new Dictionary { ["BasePath"] = "MyLibrary", - ["ContentRoot"] = Path.Combine("nuget","MyLibrary") + ["ContentRoot"] = Path.Combine("nuget", "MyLibrary"), + ["SourceId"] = "MyLibrary" }), CreateItem(Path.Combine("wwwroot", "otherLib.js"), new Dictionary { ["BasePath"] = "MyLibrary", - ["ContentRoot"] = Path.Combine("nuget", "MyOtherLibrary") + ["ContentRoot"] = Path.Combine("nuget", "MyOtherLibrary"), + ["SourceId"] = "MyOtherLibrary" }) } }; @@ -111,6 +113,58 @@ namespace Microsoft.AspNetCore.Razor.Tasks message); } + [Fact] + public void AllowsMultipleContentRootsWithSameBasePath_ForTheSameSourceId() + { + // Arrange + var file = Path.GetTempFileName(); + var expectedDocument = $@" + + +"; + + var buildEngine = new Mock(); + + var task = new GenerateStaticWebAssetsManifest + { + BuildEngine = buildEngine.Object, + ContentRootDefinitions = new TaskItem[] + { + CreateItem(Path.Combine("wwwroot","sample.js"), new Dictionary + { + ["BasePath"] = "Blazor.Client", + ["ContentRoot"] = Path.Combine(".", "nuget","Blazor.Client"), + ["SourceId"] = "Blazor.Client" + }), + CreateItem(Path.Combine("wwwroot", "otherLib.js"), new Dictionary + { + ["BasePath"] = "Blazor.Client", + ["ContentRoot"] = Path.Combine(".", "nuget", "bin","debug","netstandard2.1"), + ["SourceId"] = "Blazor.Client" + }) + }, + TargetManifestPath = file + }; + + try + { + // Act + var result = task.Execute(); + + // Assert + Assert.True(result); + var document = File.ReadAllText(file); + Assert.Equal(expectedDocument, document); + } + finally + { + if (File.Exists(file)) + { + File.Delete(file); + } + } + } + [Fact] public void ReturnsError_ForDuplicateContentRoots() { @@ -128,11 +182,13 @@ namespace Microsoft.AspNetCore.Razor.Tasks CreateItem(Path.Combine("wwwroot","sample.js"), new Dictionary { ["BasePath"] = "MyLibrary", + ["SourceId"] = "MyLibrary", ["ContentRoot"] = Path.Combine(".", "MyLibrary") }), CreateItem(Path.Combine("wwwroot", "otherLib.js"), new Dictionary { ["BasePath"] = "MyOtherLibrary", + ["SourceId"] = "MyOtherLibrary", ["ContentRoot"] = Path.Combine(".", "MyLibrary") }) } @@ -191,7 +247,7 @@ namespace Microsoft.AspNetCore.Razor.Tasks // Arrange var file = Path.GetTempFileName(); var expectedDocument = $@" - + "; try @@ -206,7 +262,8 @@ namespace Microsoft.AspNetCore.Razor.Tasks CreateItem(Path.Combine("wwwroot","sample.js"), new Dictionary { ["BasePath"] = "MyLibrary", - ["ContentRoot"] = Path.Combine(".", "nuget", "MyLibrary", "razorContent") + ["ContentRoot"] = Path.Combine(".", "nuget", "MyLibrary", "razorContent"), + ["SourceId"] = "MyLibrary" }), }, TargetManifestPath = file @@ -235,7 +292,7 @@ namespace Microsoft.AspNetCore.Razor.Tasks // Arrange var file = Path.GetTempFileName(); var expectedDocument = $@" - + "; try @@ -251,12 +308,14 @@ namespace Microsoft.AspNetCore.Razor.Tasks { // Base path needs to be normalized to '/' as it goes in the url ["BasePath"] = "Base\\MyLibrary", + ["SourceId"] = "MyLibrary", ["ContentRoot"] = Path.Combine(".", "nuget", "MyLibrary", "razorContent") }), // Comparisons are case insensitive CreateItem(Path.Combine("wwwroot, site.css"), new Dictionary { ["BasePath"] = "Base\\MyLIBRARY", + ["SourceId"] = "MyLibrary", ["ContentRoot"] = Path.Combine(".", "nuget", "MyLIBRARY", "razorContent") }), }, diff --git a/src/Razor/Microsoft.NET.Sdk.Razor/test/ValidateStaticWebAssetsUniquePathsTest.cs b/src/Razor/Microsoft.NET.Sdk.Razor/test/ValidateStaticWebAssetsUniquePathsTest.cs new file mode 100644 index 0000000000..a1a7a09337 --- /dev/null +++ b/src/Razor/Microsoft.NET.Sdk.Razor/test/ValidateStaticWebAssetsUniquePathsTest.cs @@ -0,0 +1,141 @@ +// 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 System.IO; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Razor.Tasks +{ + public class ValidateStaticWebAssetsUniquePathsTest + { + [Fact] + public void ReturnsError_WhenStaticWebAssetsWebRootPathMatchesExistingContentItemPath() + { + // Arrange + var errorMessages = new List(); + var buildEngine = new Mock(); + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + .Callback(args => errorMessages.Add(args.Message)); + + var task = new ValidateStaticWebAssetsUniquePaths + { + BuildEngine = buildEngine.Object, + StaticWebAssets = new TaskItem[] + { + CreateItem(Path.Combine(".", "Library", "wwwroot", "sample.js"), new Dictionary + { + ["BasePath"] = "/", + ["RelativePath"] = "/sample.js", + }) + }, + WebRootFiles = new TaskItem[] + { + CreateItem(Path.Combine(".", "App", "wwwroot", "sample.js"), new Dictionary + { + ["TargetPath"] = "/SAMPLE.js", + }) + } + }; + + // Act + var result = task.Execute(); + + // Assert + Assert.False(result); + var message = Assert.Single(errorMessages); + Assert.Equal($"The static web asset '{Path.Combine(".", "Library", "wwwroot", "sample.js")}' has a conflicting web root path '/SAMPLE.js' with the project file '{Path.Combine(".", "App", "wwwroot", "sample.js")}'.", message); + } + + [Fact] + public void ReturnsError_WhenMultipleStaticWebAssetsHaveTheSameWebRootPath() + { + // Arrange + var errorMessages = new List(); + var buildEngine = new Mock(); + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + .Callback(args => errorMessages.Add(args.Message)); + + var task = new ValidateStaticWebAssetsUniquePaths + { + BuildEngine = buildEngine.Object, + StaticWebAssets = new TaskItem[] + { + CreateItem(Path.Combine(".", "Library", "wwwroot", "sample.js"), new Dictionary + { + ["BasePath"] = "/", + ["RelativePath"] = "/sample.js", + }), + CreateItem(Path.Combine(".", "Library", "bin", "dist", "sample.js"), new Dictionary + { + ["BasePath"] = "/", + ["RelativePath"] = "/sample.js", + }) + } + }; + + // Act + var result = task.Execute(); + + // Assert + Assert.False(result); + var message = Assert.Single(errorMessages); + Assert.Equal($"Conflicting assets with the same path '/sample.js' for content root paths '{Path.Combine(".", "Library", "bin", "dist", "sample.js")}' and '{Path.Combine(".", "Library", "wwwroot", "sample.js")}'.", message); + } + + [Fact] + public void ReturnsSuccess_WhenStaticWebAssetsDontConflictWithApplicationContentItems() + { + // Arrange + var errorMessages = new List(); + var buildEngine = new Mock(); + + var task = new ValidateStaticWebAssetsUniquePaths + { + BuildEngine = buildEngine.Object, + StaticWebAssets = new TaskItem[] + { + CreateItem(Path.Combine(".", "Library", "wwwroot", "sample.js"), new Dictionary + { + ["BasePath"] = "/_library", + ["RelativePath"] = "/sample.js", + }), + CreateItem(Path.Combine(".", "Library", "wwwroot", "sample.js"), new Dictionary + { + ["BasePath"] = "/_library", + ["RelativePath"] = "/sample.js", + }) + }, + WebRootFiles = new TaskItem[] + { + CreateItem(Path.Combine(".", "App", "wwwroot", "sample.js"), new Dictionary + { + ["TargetPath"] = "/SAMPLE.js", + }) + } + }; + + // Act + var result = task.Execute(); + + // Assert + Assert.True(result); + } + + private static TaskItem CreateItem( + string spec, + IDictionary metadata) + { + var result = new TaskItem(spec); + foreach (var (key, value) in metadata) + { + result.SetMetadata(key, value); + } + + return result; + } + } +}