Suports discovery of static web assets from referenced projects (dotnet/aspnetcore-tooling#605)

* Define static web assets in the current project
  * These are assets under the wwwroot folder.
  * By convention, the base path for these assets is `_content/<<PackageId>>`
    with spaces and dots removed and all characters lower-cased.
* Retrieve static web assets from referenced projects\n\nCommit migrated from 50646aae64
This commit is contained in:
Javier Calvarro Nelson 2019-05-24 10:42:38 +02:00 committed by GitHub
parent 66dd6b35d3
commit 68f5fec0b0
9 changed files with 257 additions and 27 deletions

View File

@ -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;
}
}
}

View File

@ -27,17 +27,33 @@ Copyright (c) .NET Foundation. All rights reserved.
AssemblyFile="$(RazorSdkBuildTasksAssembly)"
Condition="'$(RazorSdkBuildTasksAssembly)' != ''" />
<UsingTask
TaskName="Microsoft.AspNetCore.Razor.Tasks.GetDefaultStaticWebAssetsBasePath"
AssemblyFile="$(RazorSdkBuildTasksAssembly)"
Condition="'$(RazorSdkBuildTasksAssembly)' != ''" />
<PropertyGroup>
<GenerateStaticWebAssetsManifestDependsOn>
ResolveStaticWebAssetsInputs;
_CreateStaticWebAssetsInputsCacheFile
_CreateStaticWebAssetsInputsCacheFile;
$(GenerateStaticWebAssetsManifestDependsOn)
</GenerateStaticWebAssetsManifestDependsOn>
<GetCurrentProjectStaticWebAssetsDependsOn>
ResolveStaticWebAssetsInputs;
$(GetCurrentProjectStaticWebAssetsDependsOn)
</GetCurrentProjectStaticWebAssetsDependsOn>
<AssignTargetPathsDependsOn>
GenerateStaticWebAssetsManifest;
$(AssignTargetPathsDependsOn)
</AssignTargetPathsDependsOn>
<ResolveStaticWebAssetsInputsDependsOn>
_ResolveStaticWebAssetsProjectReferences;
$(ResolveStaticWebAssetsInputsDependsOn)
</ResolveStaticWebAssetsInputsDependsOn>
</PropertyGroup>
<PropertyGroup>
@ -119,34 +135,45 @@ Copyright (c) .NET Foundation. All rights reserved.
restores the package and includes the package props file for the package.
-->
<Target
Name="ResolveStaticWebAssetsInputs">
<PropertyGroup>
<!-- The _SafeBasePath is used as a path segment in the urls that we will
be exposing content from when the library is referenced as a package
or as a project by a web application. Our convention will be to expose
content directly on _content/<<_SafeBasePath>>
We simply remove the dots from the package id so that a package
like Microsoft.AspNetCore.Identity becomes MicrosoftAspNetCoreIdentity
TODO: Investigate if we need to do something more sophisticated here.
-->
<_SafeBasePath>$(PackageId.Replace('.',''))</_SafeBasePath>
</PropertyGroup>
Name="ResolveStaticWebAssetsInputs"
DependsOnTargets="$(ResolveStaticWebAssetsInputsDependsOn)">
<!-- StaticWebAssets from the current project -->
<!-- Computes a default safe base path from the $(PackageId) that will be a prefix
to all the resources being exported from this library by default. The convention
consists of removing intermediate whitespaces, dots and lower casing all letters
in the package id using an invariant culture.
We don't aim to handle all possible cases for this prefix, as it can get really
complicated (non-unicode characters for example), so for that case,
StaticWebAssetBasePath can be set explicitly and we won't interfere.
-->
<GetDefaultStaticWebAssetsBasePath
BasePath="$(PackageId)"
Condition="'$(StaticWebAssetBasePath)' == ''">
<Output TaskParameter="SafeBasePath" PropertyName="_StaticWebAssetSafeBasePath" />
</GetDefaultStaticWebAssetsBasePath>
<PropertyGroup>
<StaticWebAssetBasePath Condition="$(StaticWebAssetBasePath) == ''">_content/$(_StaticWebAssetSafeBasePath)</StaticWebAssetBasePath>
</PropertyGroup>
<ItemGroup>
<_ThisProjectStaticWebAsset
Include="$(MSBuildProjectDirectory)\wwwroot\**"
Exclude="$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder)" />
<!--
Should we promote 'wwwroot\**'' to a property?
We don't want to capture any content outside the content root, that's why we don't do
@(Content) here.
-->
<StaticWebAsset Include="wwwroot\**" Exclude="$(DefaultItemExcludes);$(DefaultExcludesInProjectFolder)">
<!-- (PackageReference,ProjectReference,'' (CurrentProject)) -->
<StaticWebAsset Include="@(_ThisProjectStaticWebAsset)">
<!-- (Package, Project, '' (CurrentProject)) -->
<SourceType></SourceType>
<!-- Identifier describing the source, the package id, the project name, empty for the current project. -->
<SourceId></SourceId>
<SourceId>$(PackageId)</SourceId>
<!--
Full path to the content root for the item:
* For packages it corresponds to %userprofile%/.nuget/packages/<<PackageId>>/<<PackageVersion>>/razorContent
@ -155,7 +182,7 @@ Copyright (c) .NET Foundation. All rights reserved.
-->
<ContentRoot>$(MSBuildProjectDirectory)\wwwroot\</ContentRoot>
<!-- Subsection (folder) from the url space where content for this library will be served. -->
<BasePath>_content\$(_SafeBasePath)\</BasePath>
<BasePath>$(StaticWebAssetBasePath)</BasePath>
<!-- Relative path from the content root for the file. At publish time, we combine the BasePath + Relative
path to determine the final path for the file. -->
<RelativePath>%(RecursiveDir)%(FileName)%(Extension)</RelativePath>
@ -164,9 +191,57 @@ Copyright (c) .NET Foundation. All rights reserved.
</ItemGroup>
<!-- StaticWebAssets from referenced projects. -->
<!-- TODO: Include implementation -->
<MSBuild
Condition="'@(_StaticWebAssetsProjectReference->Count())' != '0'"
Projects="@(_StaticWebAssetsProjectReference)"
BuildInParallel="$(BuildInParallel)"
ContinueOnError="!$(BuildingProject)"
Targets="GetCurrentProjectStaticWebAssets"
Properties="_StaticWebAssetsSkipDependencies=true"
SkipNonexistentTargets="true">
<Output TaskParameter="TargetOutputs" ItemName="_ReferencedProjectStaticWebAssets" />
</MSBuild>
<ItemGroup>
<StaticWebAsset Include="@(_ReferencedProjectStaticWebAssets)" />
</ItemGroup>
<!-- StaticWebAssets from packages are already available, so we don't do anything. -->
</Target>
<!-- This is a helper task to compute the project references we need to invoke to retrieve
the static assets for a given application. We do it this way so that we can
pass additional build properties to compute the assets from the package when referenced
as a project. For example, Identity uses this hook to extend the project reference and
pass in the bootstrap version to use.
-->
<Target Name="_ResolveStaticWebAssetsProjectReferences"
DependsOnTargets="ResolveReferences"
Condition="'$(_StaticWebAssetsSkipDependencies)' == ''">
<ItemGroup>
<_StaticWebAssetsProjectReference Include="%(ReferencePath.MSBuildSourceProjectFile)" />
</ItemGroup>
</Target>
<!--
Child target to retrieve content from referenced projects
-->
<Target Name="GetCurrentProjectStaticWebAssets"
DependsOnTargets="$(GetCurrentProjectStaticWebAssetsDependsOn)"
Returns="@(_ThisProjectStaticWebAssets)">
<ItemGroup>
<_ThisProjectStaticWebAssets
Include="@(StaticWebAsset)"
Condition="'%(StaticWebAsset.SourceType)' == ''">
<SourceType>Project</SourceType>
</_ThisProjectStaticWebAssets>
</ItemGroup>
</Target>
</Project>

View File

@ -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<string>();
var buildEngine = new Mock<IBuildEngine>();
buildEngine.Setup(e => e.LogErrorEvent(It.IsAny<BuildErrorEventArgs>()))
.Callback<BuildErrorEventArgs>(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<string>();
var buildEngine = new Mock<IBuildEngine>();
buildEngine.Setup(e => e.LogErrorEvent(It.IsAny<BuildErrorEventArgs>()))
.Callback<BuildErrorEventArgs>(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<IBuildEngine>(),
BasePath = basePath
};
// Act
var result = task.Execute();
// Assert
Assert.True(result);
Assert.Equal(expectedSafeBasePath, task.SafeBasePath);
}
}
}

View File

@ -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 $@"<StaticWebAssets Version=""1.0"">
<ContentRoot BasePath=""_content/PackageLibraryTransitiveDependency"" Path=""{projects[0]}"" />
<ContentRoot BasePath=""_content/PackageLibraryDirectDependency"" Path=""{projects[1]}"" />
<ContentRoot BasePath=""_content/classlibrary"" Path=""{projects[2]}"" />
<ContentRoot BasePath=""_content/classlibrary2"" Path=""{projects[3]}"" />
</StaticWebAssets>";
}
}

View File

@ -11,7 +11,6 @@
$(RestoreSources);
$(RuntimeAdditionalRestoreSources)
</RestoreSources>
</PropertyGroup>
<ItemGroup>
@ -30,7 +29,11 @@
<PackageReference Include="PackageLibraryDirectDependency" Version="1.0.0" />
</ItemGroup>
<PropertyGroup Condition="'$(BinariesRoot)'==''">
<ItemGroup>
<ProjectReference Include="..\ClassLibrary2\ClassLibrary2.csproj" />
</ItemGroup>
<PropertyGroup Condition="'$(BinariesRoot)'==''">
<!-- In test scenarios $(BinariesRoot) is defined in a generated Directory.Build.props file -->
<BinariesRoot>$(RepositoryRoot)artifacts\bin\Microsoft.AspNetCore.Razor.Test.MvcShim.ClassLib\$(Configuration)\netstandard2.0\</BinariesRoot>
</PropertyGroup>

View File

@ -0,0 +1,3 @@
(function () {
document.getElementById('project-transitive-dep').innerHTML = 'project-transitive-dep';
})()

View File

@ -0,0 +1 @@
div.fluent { display: inline-block }

View File

@ -0,0 +1,3 @@
(function () {
document.getElementById('project-direct-dep').innerHTML = 'project-direct-dep';
})()