Skip to content

Commit

Permalink
Suports discovery of static web assets from referenced projects (#605)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
javiercn authored May 24, 2019
1 parent 9a96412 commit 50646aa
Show file tree
Hide file tree
Showing 9 changed files with 257 additions and 27 deletions.
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -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
Expand All @@ -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>
Expand All @@ -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>
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Microsoft.Extensions.CommandLineUtils;
using Xunit;
Expand All @@ -32,7 +33,7 @@ public StaticWebAssetsIntegrationTest(
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");
Expand Down Expand Up @@ -71,7 +72,7 @@ public async Task Build_DoesNotEmbedManifestWhen_NoStaticResourcesAvailable()
}

[Fact]
[InitializeTestProject("AppWithPackageAndP2PReference")]
[InitializeTestProject("AppWithPackageAndP2PReference",language: "C#", additionalProjects: new[] { "ClassLibrary", "ClassLibrary2" })]
public async Task Clean_Success_RemovesManifestAndCache()
{
var result = await DotnetMSBuild("Build", "/restore");
Expand All @@ -92,7 +93,7 @@ public async Task Clean_Success_RemovesManifestAndCache()
}

[Fact]
[InitializeTestProject("AppWithPackageAndP2PReference")]
[InitializeTestProject("AppWithPackageAndP2PReference",language: "C#", additionalProjects: new[] { "ClassLibrary", "ClassLibrary2" })]
public async Task Rebuild_Success_RecreatesManifestAndCache()
{
// Arrange
Expand Down Expand Up @@ -142,7 +143,7 @@ public async Task Rebuild_Success_RecreatesManifestAndCache()
}

[Fact]
[InitializeTestProject("AppWithPackageAndP2PReference")]
[InitializeTestProject("AppWithPackageAndP2PReference",language: "C#", additionalProjects: new[] { "ClassLibrary", "ClassLibrary2" })]
public async Task GenerateStaticWebAssetsManifest_IncrementalBuild_ReusesManifest()
{
var result = await DotnetMSBuild("GenerateStaticWebAssetsManifest", "/restore");
Expand Down Expand Up @@ -192,16 +193,25 @@ public Task DisposeAsync()

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>";
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
$(RestoreSources);
$(RuntimeAdditionalRestoreSources)
</RestoreSources>

</PropertyGroup>

<ItemGroup>
Expand All @@ -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>
Expand Down
Loading

0 comments on commit 50646aa

Please sign in to comment.