[Blazor] Wires up CSS isolation (#24221) (#24271)

* Wires up CSS isolation on the build.
* Transforms the css files during build.
* Bundles all scopes css into a single file and exposes it on _framework/scoped.styles.cs
* Packs pre-processed files as static web assets.
This commit is contained in:
Javier Calvarro Nelson 2020-07-24 17:42:35 +02:00 committed by GitHub
parent 11835cf768
commit b326be1710
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 979 additions and 12 deletions

View File

@ -20,6 +20,8 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests
// Arrange
// Minimal has no project references, service worker etc. This is pretty close to the project template.
using var project = ProjectDirectory.Create("blazorwasm-minimal");
File.WriteAllText(Path.Combine(project.DirectoryPath, "App.razor.css"), "h1 { font-size: 16px; }");
var result = await MSBuildProcessManager.DotnetMSBuild(project);
Assert.BuildPassed(result);
@ -35,6 +37,7 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests
var staticWebAssets = Assert.FileExists(result, buildOutputDirectory, "blazorwasm-minimal.StaticWebAssets.xml");
Assert.FileContains(result, staticWebAssets, Path.Combine(project.TargetFramework, "wwwroot"));
Assert.FileContains(result, staticWebAssets, Path.Combine(project.TargetFramework, "scopedcss"));
}
[Fact]

View File

@ -93,6 +93,58 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests
VerifyTypeGranularTrimming(result, blazorPublishDirectory);
}
[Fact]
public async Task Publish_WithScopedCss_Works()
{
// Arrange
using var project = ProjectDirectory.Create("blazorwasm", additionalProjects: new[] { "razorclasslibrary", "LinkBaseToWebRoot" });
File.WriteAllText(Path.Combine(project.DirectoryPath, "App.razor.css"), "h1 { font-size: 16px; }");
project.Configuration = "Debug";
var result = await MSBuildProcessManager.DotnetMSBuild(project, "Publish");
Assert.BuildPassed(result);
var publishDirectory = project.PublishOutputDirectory;
var blazorPublishDirectory = Path.Combine(publishDirectory, "wwwroot");
Assert.FileExists(result, blazorPublishDirectory, "_framework", "blazor.boot.json");
Assert.FileExists(result, blazorPublishDirectory, "_framework", "blazor.webassembly.js");
Assert.FileExists(result, blazorPublishDirectory, "_framework", "dotnet.wasm");
Assert.FileExists(result, blazorPublishDirectory, "_framework", DotNetJsFileName);
Assert.FileExists(result, blazorPublishDirectory, "_framework", "blazorwasm.dll");
Assert.FileExists(result, blazorPublishDirectory, "_framework", "System.Text.Json.dll"); // Verify dependencies are part of the output.
// Verify scoped css
Assert.FileExists(result, blazorPublishDirectory, "_framework", "scoped.styles.css");
// Verify referenced static web assets
Assert.FileExists(result, blazorPublishDirectory, "_content", "RazorClassLibrary", "wwwroot", "exampleJsInterop.js");
Assert.FileExists(result, blazorPublishDirectory, "_content", "RazorClassLibrary", "styles.css");
// Verify static assets are in the publish directory
Assert.FileExists(result, blazorPublishDirectory, "index.html");
// Verify link item assets are in the publish directory
Assert.FileExists(result, blazorPublishDirectory, "js", "LinkedScript.js");
var cssFile = Assert.FileExists(result, blazorPublishDirectory, "css", "app.css");
Assert.FileContains(result, cssFile, ".publish");
Assert.FileDoesNotExist(result, "dist", "Fake-License.txt");
// Verify web.config
Assert.FileExists(result, publishDirectory, "web.config");
Assert.FileCountEquals(result, 1, publishDirectory, "*", SearchOption.TopDirectoryOnly);
VerifyBootManifestHashes(result, blazorPublishDirectory);
VerifyServiceWorkerFiles(result, blazorPublishDirectory,
serviceWorkerPath: Path.Combine("serviceworkers", "my-service-worker.js"),
serviceWorkerContent: "// This is the production service worker",
assetsManifestPath: "custom-service-worker-assets.js");
VerifyTypeGranularTrimming(result, blazorPublishDirectory);
}
[Fact]
public async Task Publish_InRelease_Works()
{
@ -578,6 +630,58 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests
assetsManifestPath: "custom-service-worker-assets.js");
}
[Fact]
public async Task Publish_HostedAppWithScopedCss_VisualStudio()
{
// Simulates publishing the same way VS does by setting BuildProjectReferences=false.
// Arrange
using var project = ProjectDirectory.Create("blazorhosted", additionalProjects: new[] { "blazorwasm", "razorclasslibrary", });
File.WriteAllText(Path.Combine(project.SolutionPath, "blazorwasm", "App.razor.css"), "h1 { font-size: 16px; }");
project.Configuration = "Release";
var result = await MSBuildProcessManager.DotnetMSBuild(project, "Build", "/p:BuildInsideVisualStudio=true");
Assert.BuildPassed(result);
result = await MSBuildProcessManager.DotnetMSBuild(project, "Publish", "/p:BuildProjectReferences=false /p:BuildInsideVisualStudio=true");
var publishDirectory = project.PublishOutputDirectory;
// Make sure the main project exists
Assert.FileExists(result, publishDirectory, "blazorhosted.dll");
var blazorPublishDirectory = Path.Combine(publishDirectory, "wwwroot");
Assert.FileExists(result, blazorPublishDirectory, "_framework", "blazor.boot.json");
Assert.FileExists(result, blazorPublishDirectory, "_framework", "dotnet.wasm");
Assert.FileExists(result, blazorPublishDirectory, "_framework", DotNetJsFileName);
Assert.FileExists(result, blazorPublishDirectory, "_framework", "blazorwasm.dll");
Assert.FileExists(result, blazorPublishDirectory, "_framework", "System.Text.Json.dll"); // Verify dependencies are part of the output.
// Verify scoped css
Assert.FileExists(result, blazorPublishDirectory, "_framework", "scoped.styles.css");
// Verify static assets are in the publish directory
Assert.FileExists(result, blazorPublishDirectory, "index.html");
// Verify static web assets from referenced projects are copied.
Assert.FileExists(result, publishDirectory, "wwwroot", "_content", "RazorClassLibrary", "wwwroot", "exampleJsInterop.js");
Assert.FileExists(result, publishDirectory, "wwwroot", "_content", "RazorClassLibrary", "styles.css");
// Verify compression works
Assert.FileExists(result, blazorPublishDirectory, "_framework", "dotnet.wasm.br");
Assert.FileExists(result, blazorPublishDirectory, "_framework", "blazorwasm.dll.br");
Assert.FileExists(result, blazorPublishDirectory, "_framework", "RazorClassLibrary.dll.br");
Assert.FileExists(result, blazorPublishDirectory, "_framework", "System.Text.Json.dll.br");
// Verify web.config
Assert.FileExists(result, publishDirectory, "web.config");
VerifyBootManifestHashes(result, blazorPublishDirectory);
VerifyServiceWorkerFiles(result, blazorPublishDirectory,
serviceWorkerPath: Path.Combine("serviceworkers", "my-service-worker.js"),
serviceWorkerContent: "// This is the production service worker",
assetsManifestPath: "custom-service-worker-assets.js");
}
// Regression test to verify satellite assemblies from the blazor app are copied to the published app's wwwroot output directory as
// part of publishing in VS
[Fact]

View File

@ -246,6 +246,7 @@ Copyright (c) .NET Foundation. All rights reserved.
<GetCurrentProjectStaticWebAssetsDependsOn>
$(GetCurrentProjectStaticWebAssetsDependsOn);
AddScopedCssBundle;
_BlazorWasmPrepareForRun;
</GetCurrentProjectStaticWebAssetsDependsOn>
</PropertyGroup>

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// 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;
@ -113,4 +113,4 @@ namespace Microsoft.AspNetCore.Razor.Tools
return expandedArgs.ToArray();
}
}
}
}

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// 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;
@ -320,6 +320,9 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests
[InitializeTestProject("ClassLibrary")]
public async Task Build_TouchesUpToDateMarkerFile()
{
// Remove the components so that they don't interfere with these tests
Directory.Delete(Path.Combine(Project.DirectoryPath, "Components"), recursive: true);
var classLibraryDll = Path.Combine(IntermediateOutputPath, "ClassLibrary.dll");
var classLibraryViewsDll = Path.Combine(IntermediateOutputPath, "ClassLibrary.Views.dll");
var markerFile = Path.Combine(IntermediateOutputPath, "ClassLibrary.csproj.CopyComplete");

View File

@ -246,6 +246,7 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests
filePaths: new[]
{
Path.Combine("staticwebassets", "js", "pkg-direct-dep.js"),
Path.Combine("staticwebassets", "Components", "App.razor.rz.scp.css"),
Path.Combine("staticwebassets", "css", "site.css"),
Path.Combine("build", "Microsoft.AspNetCore.StaticWebAssets.props"),
Path.Combine("build", "PackageLibraryDirectDependency.props"),

View File

@ -0,0 +1,204 @@
// 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 System.Text.RegularExpressions;
using System.Threading.Tasks;
using Xunit;
using Xunit.Abstractions;
namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests
{
public class ScopedCssIntegrationTest : MSBuildIntegrationTestBase, IClassFixture<BuildServerTestFixture>
{
public ScopedCssIntegrationTest(
BuildServerTestFixture buildServer,
ITestOutputHelper output)
: base(buildServer)
{
Output = output;
}
public ITestOutputHelper Output { get; private set; }
[Fact]
[InitializeTestProject("ComponentApp", language: "C#")]
public async Task Build_GeneratesTransformedFilesAndBundle_ForComponentsWithScopedCss()
{
var result = await DotnetMSBuild("Build");
Assert.BuildPassed(result);
Assert.FileExists(result, IntermediateOutputPath, "scopedcss", "Components", "Pages", "Counter.razor.rz.scp.css");
Assert.FileExists(result, IntermediateOutputPath, "scopedcss", "Components", "Pages", "Index.razor.rz.scp.css");
Assert.FileExists(result, IntermediateOutputPath, "scopedcss", "_framework", "scoped.styles.css");
Assert.FileDoesNotExist(result, IntermediateOutputPath, "scopedcss", "Components", "Pages", "FetchData.razor.rz.scp.css");
}
[Fact]
[InitializeTestProject("ComponentApp", language: "C#")]
public async Task Build_ScopedCssFiles_ContainsUniqueScopesPerFile()
{
var result = await DotnetMSBuild("Build");
Assert.BuildPassed(result);
var generatedCounter = Assert.FileExists(result, IntermediateOutputPath, "scopedcss", "Components", "Pages", "Counter.razor.rz.scp.css");
var generatedIndex = Assert.FileExists(result, IntermediateOutputPath, "scopedcss", "Components", "Pages", "Index.razor.rz.scp.css");
var counterContent = File.ReadAllText(generatedCounter);
var indexContent = File.ReadAllText(generatedIndex);
var counterScopeMatch = Regex.Match(counterContent, ".*button\\[(.*)\\].*", RegexOptions.Multiline | RegexOptions.IgnoreCase);
Assert.True(counterScopeMatch.Success, "Couldn't find a scope id in the generated Counter scoped css file.");
var counterScopeId = counterScopeMatch.Groups[1].Captures[0].Value;
var indexScopeMatch = Regex.Match(indexContent, ".*h1\\[(.*)\\].*", RegexOptions.Multiline | RegexOptions.IgnoreCase);
Assert.True(indexScopeMatch.Success, "Couldn't find a scope id in the generated Index scoped css file.");
var indexScopeId = indexScopeMatch.Groups[1].Captures[0].Value;
Assert.NotEqual(counterScopeId, indexScopeId);
}
[Fact]
[InitializeTestProject("ComponentApp", language: "C#")]
public async Task Publish_PublishesBundleToTheRightLocation()
{
var result = await DotnetMSBuild("Publish");
Assert.BuildPassed(result);
Assert.FileExists(result, PublishOutputPath, "wwwroot", "_content", "ComponentApp", "_framework", "scoped.styles.css");
Assert.FileDoesNotExist(result, PublishOutputPath, "wwwroot", "_content", "ComponentApp", "Components", "Pages", "Index.razor.rz.scp.css");
Assert.FileDoesNotExist(result, PublishOutputPath, "wwwroot", "_content", "ComponentApp", "Components", "Pages", "Counter.razor.rz.scp.css");
}
[Fact]
[InitializeTestProject("ComponentApp", language: "C#")]
public async Task Publish_NoBuild_PublishesBundleToTheRightLocation()
{
var result = await DotnetMSBuild("Build");
Assert.BuildPassed(result);
result = await DotnetMSBuild("Publish", "/p:NoBuild=true");
Assert.BuildPassed(result);
Assert.FileExists(result, PublishOutputPath, "wwwroot", "_content", "ComponentApp", "_framework", "scoped.styles.css");
Assert.FileDoesNotExist(result, PublishOutputPath, "wwwroot", "_content", "ComponentApp", "Components", "Pages", "Index.razor.rz.scp.css");
Assert.FileDoesNotExist(result, PublishOutputPath, "wwwroot", "_content", "ComponentApp", "Components", "Pages", "Counter.razor.rz.scp.css");
}
[Fact]
[InitializeTestProject("ComponentApp", language: "C#")]
public async Task Publish_DoesNotPublishAnyFile_WhenThereAreNoScopedCssFiles()
{
File.Delete(Path.Combine(Project.DirectoryPath, "Components", "Pages", "Counter.razor.css"));
File.Delete(Path.Combine(Project.DirectoryPath, "Components", "Pages", "Index.razor.css"));
var result = await DotnetMSBuild("Publish");
Assert.BuildPassed(result);
Assert.FileDoesNotExist(result, PublishOutputPath, "wwwroot", "_content", "ComponentApp", "_framework", "scoped.styles.css");
}
[Fact]
[InitializeTestProject("ComponentApp", language: "C#")]
public async Task Build_GeneratedComponentContainsScope()
{
var result = await DotnetMSBuild("Build");
Assert.BuildPassed(result);
var generatedCounter = Assert.FileExists(result, IntermediateOutputPath, "scopedcss", "Components", "Pages", "Counter.razor.rz.scp.css");
Assert.FileExists(result, IntermediateOutputPath, "Razor", "Components", "Pages", "Counter.razor.g.cs");
var counterContent = File.ReadAllText(generatedCounter);
var counterScopeMatch = Regex.Match(counterContent, ".*button\\[(.*)\\].*", RegexOptions.Multiline | RegexOptions.IgnoreCase);
Assert.True(counterScopeMatch.Success, "Couldn't find a scope id in the generated Counter scoped css file.");
var counterScopeId = counterScopeMatch.Groups[1].Captures[0].Value;
Assert.FileContains(result, Path.Combine(IntermediateOutputPath, "Razor", "Components", "Pages", "Counter.razor.g.cs"), counterScopeId);
}
[Fact]
[InitializeTestProject("ComponentApp", language: "C#")]
public async Task Build_RemovingScopedCssAndBuilding_UpdatesGeneratedCodeAndBundle()
{
var result = await DotnetMSBuild("Build");
Assert.BuildPassed(result);
Assert.FileExists(result, IntermediateOutputPath, "scopedcss", "Components", "Pages", "Counter.razor.rz.scp.css");
var generatedBundle = Assert.FileExists(result, IntermediateOutputPath, "scopedcss", "_framework", "scoped.styles.css");
var generatedCounter = Assert.FileExists(result, IntermediateOutputPath, "Razor", "Components", "Pages", "Counter.razor.g.cs");
var componentThumbprint = GetThumbPrint(generatedCounter);
var bundleThumbprint = GetThumbPrint(generatedBundle);
File.Delete(Path.Combine(Project.DirectoryPath, "Components", "Pages", "Counter.razor.css"));
result = await DotnetMSBuild("Build");
Assert.BuildPassed(result);
Assert.FileDoesNotExist(result, IntermediateOutputPath, "scopedcss", "Components", "Pages", "Counter.razor.rz.scp.css");
generatedCounter = Assert.FileExists(result, IntermediateOutputPath, "Razor", "Components", "Pages", "Counter.razor.g.cs");
var newComponentThumbprint = GetThumbPrint(generatedCounter);
var newBundleThumbprint = GetThumbPrint(generatedBundle);
Assert.NotEqual(componentThumbprint, newComponentThumbprint);
Assert.NotEqual(bundleThumbprint, newBundleThumbprint);
}
[Fact]
[InitializeTestProject("ComponentApp", language: "C#")]
public async Task Does_Nothing_WhenThereAreNoScopedCssFiles()
{
File.Delete(Path.Combine(Project.DirectoryPath, "Components", "Pages", "Counter.razor.css"));
File.Delete(Path.Combine(Project.DirectoryPath, "Components", "Pages", "Index.razor.css"));
var result = await DotnetMSBuild("Build");
Assert.BuildPassed(result);
Assert.FileDoesNotExist(result, IntermediateOutputPath, "scopedcss", "Components", "Pages", "Counter.razor.rz.scp.css");
Assert.FileDoesNotExist(result, IntermediateOutputPath, "scopedcss", "Components", "Pages", "Index.razor.rz.scp.css");
Assert.FileDoesNotExist(result, IntermediateOutputPath, "scopedcss", "_framework", "scoped.styles.css");
}
[Fact]
[InitializeTestProject("ComponentApp", language: "C#")]
public async Task Build_ScopedCssTransformation_AndBundling_IsIncremental()
{
// Arrange
var thumbprintLookup = new Dictionary<string, FileThumbPrint>();
// Act 1
var result = await DotnetMSBuild("Build");
var directoryPath = Path.Combine(result.Project.DirectoryPath, IntermediateOutputPath, "scopedcss");
var files = Directory.GetFiles(directoryPath, "*", SearchOption.AllDirectories);
foreach (var file in files)
{
var thumbprint = GetThumbPrint(file);
thumbprintLookup[file] = thumbprint;
}
// Assert 1
Assert.BuildPassed(result);
// Act & Assert 2
for (var i = 0; i < 2; i++)
{
// We want to make sure nothing changed between multiple incremental builds.
using (var razorGenDirectoryLock = LockDirectory(RazorIntermediateOutputPath))
{
result = await DotnetMSBuild("Build");
}
Assert.BuildPassed(result);
foreach (var file in files)
{
var thumbprint = GetThumbPrint(file);
Assert.Equal(thumbprintLookup[file], thumbprint);
}
}
}
}
}

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// 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;
@ -72,6 +72,7 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests
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"));
Assert.FileExists(result, PublishOutputPath, Path.Combine("wwwroot", "_content", "AppWithPackageAndP2PReference", "_framework", "scoped.styles.css"));
// 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"));
@ -97,6 +98,7 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests
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"));
Assert.FileExists(result, publishOutputPath, Path.Combine("wwwroot", "_content", "AppWithPackageAndP2PReferenceAndRID", "_framework", "scoped.styles.css"));
// 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"));
@ -126,6 +128,7 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests
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"));
Assert.FileExists(publish, PublishOutputPath, Path.Combine("wwwroot", "_content", "AppWithPackageAndP2PReference", "_framework", "scoped.styles.css"));
}
[Fact]
@ -148,6 +151,7 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests
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"));
Assert.FileExists(publish, PublishOutputPath, Path.Combine("wwwroot", "_content", "AppWithPackageAndP2PReference", "_framework", "scoped.styles.css"));
}
[Fact]
@ -294,13 +298,15 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests
{
Path.Combine(restorePath, "packagelibrarytransitivedependency", "1.0.0", "build", "..", "staticwebassets") + Path.DirectorySeparatorChar,
Path.Combine(restorePath, "packagelibrarydirectdependency", "1.0.0", "build", "..", "staticwebassets") + Path.DirectorySeparatorChar,
Path.GetFullPath(Path.Combine(source, "ClassLibrary2", "wwwroot")) + Path.DirectorySeparatorChar,
Path.GetFullPath(Path.Combine(source, "ClassLibrary", "wwwroot")) + Path.DirectorySeparatorChar,
Path.GetFullPath(Path.Combine(source, "ClassLibrary2", "wwwroot")) + Path.DirectorySeparatorChar
Path.GetFullPath(Path.Combine(source, "AppWithPackageAndP2PReference", IntermediateOutputPath, "scopedcss")) + Path.DirectorySeparatorChar,
};
return $@"<StaticWebAssets Version=""1.0"">
<ContentRoot BasePath=""_content/ClassLibrary"" Path=""{projects[2]}"" />
<ContentRoot BasePath=""_content/ClassLibrary2"" Path=""{projects[3]}"" />
<ContentRoot BasePath=""_content/AppWithPackageAndP2PReference"" Path=""{projects[4]}"" />
<ContentRoot BasePath=""_content/ClassLibrary"" Path=""{projects[3]}"" />
<ContentRoot BasePath=""_content/ClassLibrary2"" Path=""{projects[2]}"" />
<ContentRoot BasePath=""_content/PackageLibraryDirectDependency"" Path=""{projects[1]}"" />
<ContentRoot BasePath=""_content/PackageLibraryTransitiveDependency"" Path=""{projects[0]}"" />
</StaticWebAssets>";

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;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
namespace Microsoft.AspNetCore.Razor.Tasks
{
public class ApplyCssScopes : Task
{
[Required]
public ITaskItem[] RazorComponents { get; set; }
[Required]
public ITaskItem[] ScopedCss { get; set; }
[Output]
public ITaskItem[] RazorComponentsWithScopes { get; set; }
public override bool Execute()
{
var razorComponentsWithScopes = new List<ITaskItem>();
var unmatchedScopedCss = new List<ITaskItem>(ScopedCss);
var scopedCssByComponent = new Dictionary<string, IList<ITaskItem>>();
for (var i = 0; i < RazorComponents.Length; i++)
{
var componentCandidate = RazorComponents[i];
var j = 0;
while (j < unmatchedScopedCss.Count)
{
var scopedCssCandidate = unmatchedScopedCss[j];
var explicitRazorcomponent = scopedCssCandidate.GetMetadata("RazorComponent");
var razorComponent = !string.IsNullOrWhiteSpace(explicitRazorcomponent) ?
explicitRazorcomponent :
Regex.Replace(scopedCssCandidate.ItemSpec, "(.*)\\.razor\\.css$", "$1.razor", RegexOptions.IgnoreCase);
if (string.Equals(componentCandidate.ItemSpec, razorComponent, StringComparison.OrdinalIgnoreCase))
{
unmatchedScopedCss.RemoveAt(j);
if (!scopedCssByComponent.TryGetValue(componentCandidate.ItemSpec, out var existing))
{
scopedCssByComponent[componentCandidate.ItemSpec] = new List<ITaskItem>() { scopedCssCandidate };
var item = new TaskItem(componentCandidate);
item.SetMetadata("CssScope", scopedCssCandidate.GetMetadata("CssScope"));
razorComponentsWithScopes.Add(item);
}
else
{
existing.Add(scopedCssCandidate);
}
}
else
{
j++;
}
}
}
foreach (var kvp in scopedCssByComponent)
{
var component = kvp.Key;
var scopeFiles = kvp.Value;
if (scopeFiles.Count > 1)
{
Log.LogError($"More than one scoped css files were found for the razor component '{component}'. Each razor component must have at most" +
" a single associated scoped css file." + Environment.NewLine + string.Join(Environment.NewLine, scopeFiles.Select(f => f.ItemSpec)));
}
}
// We don't want to allow scoped css files without a matching component. Our convention is very specific in its requirements
// so failing to have a matching component very likely means an error.
// When the matching component was specified explicitly, failing to find a matching component is an error.
// This simplifies a few things like being able to assume that the presence of a .razor.css file or a ScopedCssInput item will result in a bundle being produced,
// that the contents of the bundle are independent of the existence of a component and that users will be able to catch errors at compile
// time instead of wondering why their component doesn't have a scope applied to it.
// In the rare case that a .razor file exists on the user project, has an associated .razor.css file and the user decides to exclude it as a RazorComponent they
// can update the Content item for the .razor.css file with Scoped=false and we will not consider it.
foreach (var unmatched in unmatchedScopedCss)
{
Log.LogError($"The scoped css file '{unmatched.ItemSpec}' was defined but no associated razor component was found for it.");
}
RazorComponentsWithScopes = razorComponentsWithScopes.ToArray();
return !Log.HasLoggedErrors;
}
}
}

View File

@ -0,0 +1,71 @@
// 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.Numerics;
using System.Security.Cryptography;
using System.Text;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
namespace Microsoft.AspNetCore.Razor.Tasks
{
public class ComputeCssScope : Task
{
[Required]
public ITaskItem[] ScopedCssInput { get; set; }
[Required]
public string TargetName { get; set; }
[Output]
public ITaskItem[] ScopedCss { get; set; }
public override bool Execute()
{
ScopedCss = new ITaskItem[ScopedCssInput.Length];
for (var i = 0; i < ScopedCssInput.Length; i++)
{
var input = ScopedCssInput[i];
// Todo: Normalize path to forward slashes and lowercase before computing the hash
var relativePath = input.ItemSpec.Replace("\\","//");
var scope = input.GetMetadata("CssScope");
scope = !string.IsNullOrEmpty(scope) ? scope : GenerateScope(TargetName, relativePath);
var outputItem = new TaskItem(input);
outputItem.SetMetadata("CssScope", scope);
ScopedCss[i] = outputItem;
}
return !Log.HasLoggedErrors;
}
private string GenerateScope(string targetName, string relativePath)
{
using var hash = SHA256.Create();
var bytes = Encoding.UTF8.GetBytes(relativePath + targetName);
var hashBytes = hash.ComputeHash(bytes);
var builder = new StringBuilder();
builder.Append("b-");
builder.Append(ToBase36(hashBytes));
return builder.ToString();
}
private string ToBase36(byte[] hash)
{
var builder = new StringBuilder();
const string chars = "abcdefghijklmnopqrstuvwxyz0123456789";
var dividend = new BigInteger(hash.AsSpan().Slice(0,8).ToArray());
while (dividend > 36)
{
dividend = BigInteger.DivRem(dividend, 36, out var remainder);
builder.Insert(0, chars[Math.Abs(((int)remainder))]);
}
return builder.ToString();
}
}
}

View File

@ -0,0 +1,72 @@
// 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.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
namespace Microsoft.AspNetCore.Razor.Tasks
{
public class ConcatenateCssFiles : Task
{
[Required]
public ITaskItem[] FilesToProcess { get; set; }
[Required]
public string OutputFile { get; set; }
public override bool Execute()
{
var builder = new StringBuilder();
var orderedFiles = FilesToProcess.OrderBy(f => f.GetMetadata("FullPath")).ToArray();
for (var i = 0; i < orderedFiles.Length; i++)
{
var current = orderedFiles[i];
builder.AppendLine($"/* {current.GetMetadata("BasePath").Replace("\\","/")}{current.GetMetadata("RelativePath").Replace("\\","/")} */");
foreach (var line in File.ReadLines(FilesToProcess[i].GetMetadata("FullPath")))
{
builder.AppendLine(line);
}
}
var content = builder.ToString();
if (!File.Exists(OutputFile) || !SameContent(content, OutputFile))
{
Directory.CreateDirectory(Path.GetDirectoryName(OutputFile));
File.WriteAllText(OutputFile, content);
}
return !Log.HasLoggedErrors;
}
private bool SameContent(string content, string outputFilePath)
{
var contentHash = GetContentHash(content);
var outputContent = File.ReadAllText(outputFilePath);
var outputContentHash = GetContentHash(outputContent);
for (int i = 0; i < outputContentHash.Length; i++)
{
if (outputContentHash[i] != contentHash[i])
{
return false;
}
}
return true;
static byte[] GetContentHash(string content)
{
using var sha256 = SHA256.Create();
return sha256.ComputeHash(Encoding.UTF8.GetBytes(content));
}
}
}
}

View File

@ -0,0 +1,39 @@
// 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 Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
namespace Microsoft.AspNetCore.Razor.Tasks
{
public class DiscoverDefaultScopedCssItems : Task
{
[Required]
public ITaskItem[] Content { get; set; }
[Output]
public ITaskItem[] DiscoveredScopedCssInputs { get; set; }
public override bool Execute()
{
var discoveredInputs = new List<ITaskItem>();
for (var i = 0; i < Content.Length; i++)
{
var candidate = Content[i];
var fullPath = candidate.GetMetadata("FullPath");
if (fullPath.EndsWith(".razor.css", StringComparison.OrdinalIgnoreCase) &&
!string.Equals("false", candidate.GetMetadata("Scoped"), StringComparison.OrdinalIgnoreCase))
{
discoveredInputs.Add(candidate);
}
}
DiscoveredScopedCssInputs = discoveredInputs.ToArray();
return !Log.HasLoggedErrors;
}
}
}

View File

@ -1,4 +1,4 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
@ -106,7 +106,10 @@ namespace Microsoft.AspNetCore.Razor.Tasks
if (!ValidateMetadataMatches(firstAsset, webAsset, SourceId) ||
!ValidateMetadataMatches(firstAsset, webAsset, SourceType) ||
!ValidateMetadataMatches(firstAsset, webAsset, ContentRoot) ||
// Now that we support generated assets we need to be able to support multiple content roots.
// We need to change this check for one that ensures that no two files end up in the same final destination
//!ValidateMetadataMatches(firstAsset, webAsset, ContentRoot) ||
// See https://github.com/dotnet/aspnetcore/issues/24257
!ValidateMetadataMatches(firstAsset, webAsset, BasePath))
{
return false;
@ -156,4 +159,4 @@ namespace Microsoft.AspNetCore.Razor.Tasks
return false;
}
}
}
}

View File

@ -0,0 +1,38 @@
// 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 Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
namespace Microsoft.AspNetCore.Razor.Tasks
{
public class ResolveAllScopedCssAssets : Task
{
[Required]
public ITaskItem[] StaticWebAssets { get; set; }
[Output]
public ITaskItem[] ScopedCssAssets { get; set; }
public override bool Execute()
{
var scopedCssAssets = new List<ITaskItem>();
for (var i = 0; i < StaticWebAssets.Length; i++)
{
var swa = StaticWebAssets[i];
var fullPath = swa.GetMetadata("RelativePath");
if (fullPath.EndsWith(".rz.scp.css", StringComparison.OrdinalIgnoreCase))
{
scopedCssAssets.Add(swa);
}
}
ScopedCssAssets = scopedCssAssets.ToArray();
return !Log.HasLoggedErrors;
}
}
}

View File

@ -0,0 +1,59 @@
// 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.IO;
using System.Text;
using Microsoft.Build.Framework;
namespace Microsoft.AspNetCore.Razor.Tasks
{
public class RewriteCss : DotNetToolTask
{
[Required]
public ITaskItem[] FilesToTransform { get; set; }
public bool SkipIfOutputIsNewer { get; set; } = true;
internal override string Command => "rewritecss";
protected override string GenerateResponseFileCommands()
{
var builder = new StringBuilder();
builder.AppendLine(Command);
for (var i = 0; i < FilesToTransform.Length; i++)
{
var input = FilesToTransform[i];
var inputFullPath = input.GetMetadata("FullPath");
var relativePath = input.GetMetadata("RelativePath");
var cssScope = input.GetMetadata("CssScope");
var outputPath = input.GetMetadata("OutputFile");
if (SkipIfOutputIsNewer && File.Exists(outputPath) && File.GetLastWriteTimeUtc(inputFullPath) < File.GetLastWriteTimeUtc(outputPath))
{
Log.LogMessage(MessageImportance.Low, $"Skipping scope transformation for '{input.ItemSpec}' because '{outputPath}' is newer than '{input.ItemSpec}'.");
continue;
}
builder.AppendLine("-s");
builder.AppendLine(inputFullPath);
builder.AppendLine("-o");
builder.AppendLine(outputPath);
// Create the directory for the output file in case it doesn't exist.
// Its easier to do it here than on MSBuild. Alternatively the tool could have taken care of it.
Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
builder.AppendLine("-c");
builder.AppendLine(cssScope);
}
return builder.ToString();
}
internal static string CalculateTargetPath(string relativePath, string extension) =>
Path.ChangeExtension(relativePath, $"{extension}{Path.GetExtension(relativePath)}");
}
}

View File

@ -0,0 +1,244 @@
<!--
***********************************************************************************************
Microsoft.NET.Sdk.Razor.ScopedCss.targets
WARNING: DO NOT MODIFY this file unless you are knowledgeable about MSBuild and have
created a backup copy. Incorrect changes to this file will make it
impossible to load or build your projects from the command-line or the IDE.
Copyright (c) .NET Foundation. All rights reserved.
***********************************************************************************************
-->
<Project ToolsVersion="14.0">
<!-- General description of the scoped CSS pipeline and its integration with static web assets:
* Scoped css files get discovered and put into a ScopedCssInput itemgroup.
* Any file with a *.razor.css extension gets processed as a scoped css file. That means two things:
* A uniquely identifying scope attribute is generated for that file.
* The file will be transformed to apply the unique scope to all selectors and a new file will be generated.
* This new file along with the scope will be added to the ScopedCss itemgroup.
* When resolving Razor inputs we will match RazorComponent items with their associated ScopedCss item by convention.
* The convention is that the scoped css file will have to have the same full path as the razor file with the addition of the .css extension.
* Users can define their own convention by adding their own ScopedCssInput item with the RazorComponent metadata on it.
* This metadata will point to the item spec for a given RazorComponent (typically the path from the root of the project)
* At this point, if a razor.css file doesn't have an associated RazorComponent it will be discarded and not included in the final bundle.
* This makes sure that the scoped css pipeline and the components pipeline are as orthogonal as possible.
* Computing the scopes will happen very early on the pipeline and it will generate all the input that the compiler needs to do its job
independently.
* For web applications (Blazor webassembly and Blazor server) the main project is responsible for producing the final CSS bundle and making
it available during development and production behind _framework/scoped.styles.css
* For razor class libraries we will add the list of ScopedCss to the list of available static web assets imported by the project, the main project
will then discover these assets and add them to the ScopedCss files to process in the final bundle.
* For packing in razor class libraries, the ScopedCss files will get processed and added as static web assets to the pack.
Integration with static web assets:
* The generated scoped css files will be added as regular static web assets to participate in the pipeline.
* Generated scoped css files will have a unique extension '.rz.scp.css' that will be used by the pipeline to identify them as such.
* In razor class libraries these generated files will be packaged normally as part of the static web assets process and if bundling is
not enabled would be normally accessible at <<StaticWebAssetsBasePath>>/<<RelativePath>>.
* When bundling is enabled (there's no actual way to disable it) all scoped css files from class libraries will be identified by looking
at the list of static web assets and identifying the ones that have a .rz.scp.css extension.
* Using the extension is useful as it allows for third party tooling to do alternative processing in an easy way, these files will be
removed off from the list of static web assets when the default bundling is enabled, so they won't show up in the final output.
-->
<UsingTask TaskName="Microsoft.AspNetCore.Razor.Tasks.DiscoverDefaultScopedCssItems" AssemblyFile="$(RazorSdkBuildTasksAssembly)" />
<UsingTask TaskName="Microsoft.AspNetCore.Razor.Tasks.ResolveAllScopedCssAssets" AssemblyFile="$(RazorSdkBuildTasksAssembly)" />
<UsingTask TaskName="Microsoft.AspNetCore.Razor.Tasks.ApplyCssScopes" AssemblyFile="$(RazorSdkBuildTasksAssembly)" />
<UsingTask TaskName="Microsoft.AspNetCore.Razor.Tasks.ComputeCssScope" AssemblyFile="$(RazorSdkBuildTasksAssembly)" />
<UsingTask TaskName="Microsoft.AspNetCore.Razor.Tasks.RewriteCss" AssemblyFile="$(RazorSdkBuildTasksAssembly)" />
<UsingTask TaskName="Microsoft.AspNetCore.Razor.Tasks.ConcatenateCssFiles" AssemblyFile="$(RazorSdkBuildTasksAssembly)" />
<PropertyGroup>
<!-- We are going to use .rz.scp.css as the extension to mark scoped css files that come from packages or that have been pre-procesed by
referenced class libraries. This way, we can use that information to adjust the build pipeline without having to rely on external
sources like an additional itemgroup or metadata.
-->
<_ScopedCssExtension>.rz.scp.css</_ScopedCssExtension>
<ResolveStaticWebAssetsInputsDependsOn>$(ResolveStaticWebAssetsInputsDependsOn);_CollectAllScopedCssAssets;AddScopedCssBundle</ResolveStaticWebAssetsInputsDependsOn>
<ResolveCurrentProjectStaticWebAssetsInputsDependsOn>$(ResolveCurrentProjectStaticWebAssetsInputsDependsOn);_AddGeneratedScopedCssFiles</ResolveCurrentProjectStaticWebAssetsInputsDependsOn>
</PropertyGroup>
<Target Name="ResolveScopedCssInputs">
<!--
Gathers input source files for Razor component generation. This is a separate target so that we can avoid
lots of work when there are no inputs for code generation.
NOTE: This target is called as part of an incremental build scenario in VS. Do not perform any work
outside of calculating RazorComponent items in this target.
-->
<DiscoverDefaultScopedCssItems Condition="'$(EnableDefaultScopedCssItems)'=='true'" Content="@(None);@(Content)">
<Output TaskParameter="DiscoveredScopedCssInputs" ItemName="_DiscoveredScopedCssInputs" />
</DiscoverDefaultScopedCssItems>
<ItemGroup Condition="'$(EnableDefaultScopedCssItems)'=='true'">
<ScopedCssInput Include="@(_DiscoveredScopedCssInputs)" />
</ItemGroup>
<ItemGroup>
<Content Remove="@(ScopedCssInput)" />
<Content Include="@(ScopedCssInput)" CopyToPublishDirectory="Never" />
</ItemGroup>
</Target>
<!-- This target just generates a Scope identifier for the items that we deemed were scoped css files -->
<Target Name="_ComputeCssScope" DependsOnTargets="ResolveScopedCssInputs">
<ComputeCssScope ScopedCssInput="@(ScopedCssInput)" Targetname="$(TargetName)">
<Output TaskParameter="ScopedCss" ItemName="_ScopedCss" />
</ComputeCssScope>
</Target>
<!-- This target validates that there is at most one scoped css file per component, that there are no scoped css files without a
matching component, and then adds the associated scope to the razor components that have a matching scoped css file.
-->
<Target Name="_ResolveComponentCssScopes" BeforeTargets="AssignRazorComponentTargetPaths" DependsOnTargets="_ComputeCssScope;ResolveRazorComponentInputs">
<ApplyCssScopes RazorComponents="@(RazorComponent)" ScopedCss="@(_ScopedCss)">
<Output TaskParameter="RazorComponentsWithScopes" ItemName="_RazorComponentsWithScopes" />
</ApplyCssScopes>
<ItemGroup>
<RazorComponent Remove="@(_RazorComponentsWithScopes)" />
<RazorComponent Include="@(_RazorComponentsWithScopes)" />
</ItemGroup>
</Target>
<!-- Sets the output path for the processed scoped css files. They will all have a '.rz.scp.css' extension to flag them as processed
scoped css files. -->
<Target Name="_ResolveScopedCssOutputs" DependsOnTargets="_ComputeCssScope">
<PropertyGroup>
<_ScopedCssIntermediatePath>$([System.IO.Path]::GetFullPath($(IntermediateOutputPath)scopedcss\))</_ScopedCssIntermediatePath>
</PropertyGroup>
<ItemGroup>
<_ScopedCss Condition="'%(_ScopedCss.Identity)' != ''">
<OutputFile>$(_ScopedCssIntermediatePath)%(RelativeDir)%(RecursiveDir)%(FileName)$(_ScopedCssExtension)</OutputFile>
</_ScopedCss>
<_ScopedCssOutputs Include="%(_ScopedCss.OutputFile)" />
</ItemGroup>
</Target>
<!-- Transforms the original scoped CSS files into their scoped versions on their designated output paths -->
<Target Name="_GenerateScopedCssFiles" Inputs="@(_ScopedCss)" Outputs="@(_ScopedCssOutputs)" DependsOnTargets="_ResolveScopedCssOutputs">
<MakeDir Directories="$(_ScopedCssIntermediatePath)" />
<RewriteCss
FilesToTransform="@(_ScopedCss)"
ToolAssembly="$(_RazorSdkToolAssembly)">
</RewriteCss>
<ItemGroup>
<FileWrites Include="%(_ScopedCss.OutputFile)" />
</ItemGroup>
</Target>
<!--
This target is added to ResolveStaticWebAssetInputs which only gets called by the main application.
This makes sure we only include the bundle file when we are processing an application for build/publish
and avoids including it on razor class libraries.
In the hosted blazor webassembly case, we want to include the bundle within the assets returned to the host, so we wire up this task
to `GetCurrentProjectStaticWebAssetsDependsOn` so that contents are replaced and shared with the host application.
Normally, _CollectAllScopedCssAssets will find all the scoped css files from referenced packages, class libraries and the current project. When AddScopedCssBundle
runs, it will remove all those static web assets and add the bundle asset.
When _CollectAllScopedCssAssets runs as part of a hosted blazor webassembly app, only the current project and package assets are removed from the list of
static web assets. If the host also decides to generate a bundle, there will be a bundle for the razor client app and another bundle for the host and they will
contain some overlapping css.
* The bundle for the client app will contain the transitive closure of the processed css files for the client app.
* The bundle for the server app will contain the css for the referenced class libraries (transitively and the packages).
* Users in this position can choose to remove CssScopedInput entries to avoid including them in the host bundle.
For Blazor webassembly we want to trigger the bundling at the Blazor client level so that different applications can have self-contained bundles. For the most
common case, the bundle for a Blazor app and its host should be identical modulo path comments on the bundle.
If one single bundle is desired, bundling can be disabled in the Blazor application and the host will create a single big bundle file.
-->
<Target Name="AddScopedCssBundle" Condition="'$(ScopedCssDisableBundling)' != 'true'" DependsOnTargets="_CollectAllScopedCssAssets">
<PropertyGroup>
<_ScopedCssOutputPath>$(_ScopedCssIntermediatePath)_framework\scoped.styles.css</_ScopedCssOutputPath>
<_ScopedCssOutputFullPath>$([System.IO.Path]::Combine('$(MSBuildProjectFileDirectory)', '$(_ScopedCssIntermediatePath)_framework\scoped.styles.css'))</_ScopedCssOutputFullPath>
</PropertyGroup>
<ItemGroup>
<!-- When bundling is enabled we want to remove all identified generated scoped css files from the list of static web assets so that
they are not copied to the output folder. -->
<StaticWebAsset Remove="@(_AllScopedCss)" Condition="'$(ScopedCssDisableBundling)' != 'true'" />
<!-- https://github.com/dotnet/aspnetcore/issues/24245 -->
<StaticWebAsset Include="$(_ScopedCssOutputPath)" Condition="@(_AllScopedCss) != ''">
<SourceType></SourceType>
<SourceId>$(PackageId)</SourceId>
<ContentRoot>$(_ScopedCssIntermediatePath)</ContentRoot>
<BasePath>$(StaticWebAssetBasePath)</BasePath>
<RelativePath>_framework/scoped.styles.css</RelativePath>
</StaticWebAsset>
<_ExternalStaticWebAsset Include="$(_ScopedCssOutputPath)" Condition="@(_AllScopedCss) != ''">
<SourceType>generated</SourceType>
<SourceId>$(PackageId)</SourceId>
<ContentRoot>$(_ScopedCssIntermediatePath)</ContentRoot>
<BasePath>$(StaticWebAssetBasePath)</BasePath>
<RelativePath>_framework/scoped.styles.css</RelativePath>
</_ExternalStaticWebAsset>
</ItemGroup>
</Target>
<!-- This target runs as part of ResolveStaticWebAssetInputs and collects all the generated scoped css files. When bundling is enabled
these files are removed from the list of static web assets by '_AddScopedCssBundle' -->
<Target Name="_CollectAllScopedCssAssets">
<ResolveAllScopedCssAssets StaticWebAssets="@(StaticWebAsset)">
<Output TaskParameter="ScopedCssAssets" ItemName="_AllScopedCss" />
</ResolveAllScopedCssAssets>
</Target>
<!-- This target is only called as part of GetCurrentProjectStaticWebAssets which is only invoked on referenced projects to get the list
of their assets. We return the list of css outputs we will produce and let the main app do the final bundling. -->
<Target Name="_AddGeneratedScopedCssFiles" DependsOnTargets="_ResolveScopedCssOutputs">
<PropertyGroup>
<StaticWebAssetBasePath Condition="$(StaticWebAssetBasePath) == ''">_content/$(PackageId)</StaticWebAssetBasePath>
</PropertyGroup>
<ItemGroup>
<StaticWebAsset Include="%(_ScopedCss.OutputFile)" Condition="@(_ScopedCss) != ''">
<SourceType></SourceType>
<SourceId>$(PackageId)</SourceId>
<ContentRoot>$(IntermediateOutputPath)scopedcss\</ContentRoot>
<BasePath>$(StaticWebAssetBasePath)</BasePath>
<RelativePath>$([MSBuild]::MakeRelative('$(_ScopedCssIntermediatePath)','%(_ScopedCss.OutputFile)'))</RelativePath>
</StaticWebAsset>
</ItemGroup>
</Target>
<Target Name="BundleScopedCssFiles" Condition="'$(ScopedCssDisableBundling)' != 'true' and '@(_AllScopedCss)' != ''" BeforeTargets="GetCopyToOutputDirectoryItems;_StaticWebAssetsComputeFilesToPublish" DependsOnTargets="_GenerateScopedCssFiles">
<!-- Incrementalism is built into the task itself. -->
<ConcatenateCssFiles FilesToProcess="@(_AllScopedCss)" OutputFile="$(_ScopedCssOutputPath)" />
</Target>
<Target Name="_RemoveBundleFromOutput" BeforeTargets="GetCopyToOutputDirectoryItems" DependsOnTargets="BundleScopedCssFiles">
<ItemGroup>
<StaticWebAsset Remove="$(_ScopedCssOutputFullPath)" />
<StaticWebAsset Include="@(_AllScopedCss)" Condition="'%(SourceType)' == ''" />
</ItemGroup>
</Target>
<Target Name="_AddBundleToStaticWebAssetsPublishedFile" Condition="'$(ScopedCssDisableBundling)' != 'true' and '@(_AllScopedCss)' != ''" BeforeTargets="_StaticWebAssetsComputeFilesToPublish" DependsOnTargets="_CollectAllScopedCssAssets">
<ItemGroup>
<!-- Manually add the file to the publish flow. See https://github.com/dotnet/aspnetcore/issues/24245 -->
<_ExternalPublishStaticWebAsset Include="$(_ScopedCssOutputFullPath)" ExcludeFromSingleFile="true">
<SourceType>generated</SourceType>
<SourceId>$(PackageId)</SourceId>
<ContentRoot>$(_ScopedCssIntermediatePath)</ContentRoot>
<BasePath>$(StaticWebAssetBasePath)</BasePath>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
<RelativePath>$([MSBuild]::MakeRelative('$(MSBuildProjectDirectory)',$([MSBuild]::NormalizePath('wwwroot/$(StaticWebAssetBasePath)/_framework/scoped.styles.css'))))</RelativePath>
</_ExternalPublishStaticWebAsset>
</ItemGroup>
</Target>
<Target Name="_AdjustIsolatedCssPackageContents" BeforeTargets="_RemoveWebRootContentFromPackaging;_CreateStaticWebAssetsCustomPropsCacheFile" DependsOnTargets="_CollectAllScopedCssAssets">
<ItemGroup>
<_CurrentProjectStaticWebAsset Remove="$(_ScopedCssOutputFullPath)" />
<StaticWebAsset Remove="$(_ScopedCssOutputFullPath)" />
<StaticWebAsset Include="@(_AllScopedCss)" Condition="'%(SourceType)' == ''" />
</ItemGroup>
</Target>
</Project>

View File

@ -36,6 +36,11 @@ Copyright (c) .NET Foundation. All rights reserved.
-->
<EnableDefaultRazorComponentItems Condition="'$(EnableDefaultRazorComponentItems)'==''">true</EnableDefaultRazorComponentItems>
<!--
Set to true to automatically include Razor (.razor.cs) files in @(ScopedCssInput) from @(Content).
-->
<EnableDefaultScopedCssItems Condition="'$(EnableDefaultScopedCssItems)'==''">true</EnableDefaultScopedCssItems>
<!--
Set to true to copy RazorGenerate items (.cshtml) to the publish directory.

View File

@ -64,6 +64,9 @@ Copyright (c) .NET Foundation. All rights reserved.
<!-- Controls whether or not the static web assets feature is enabled. By default is enabled for netcoreapp3.0
applications and RazorLangVersion 3 or above. -->
<StaticWebAssetsEnabled Condition="'$(StaticWebAssetsEnabled)' == ''">$(_Targeting30OrNewerRazorLangVersion)</StaticWebAssetsEnabled>
<!-- Controls whether or not the scoped css feature is enabled. By default is enabled for net5.0 applications and RazorLangVersion 5 or above -->
<ScopedCssEnabled Condition="'$(ScopedCssEnabled)' == ''">$(_Targeting30OrNewerRazorLangVersion)</ScopedCssEnabled>
</PropertyGroup>
@ -353,6 +356,8 @@ Copyright (c) .NET Foundation. All rights reserved.
<Import Project="Microsoft.NET.Sdk.Razor.StaticWebAssets.targets" Condition="'$(StaticWebAssetsEnabled)' == 'true'" />
<Import Project="Microsoft.NET.Sdk.Razor.ScopedCss.targets" Condition="'$(ScopedCssEnabled)' == 'true'" />
<Import Project="Microsoft.NET.Sdk.Razor.GenerateAssemblyInfo.targets" />
<Import Project="Microsoft.NET.Sdk.Razor.MvcApplicationPartsDiscovery.targets" Condition="'$(_TargetingNETCoreApp30OrLater)' == 'true'" />

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// 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;
@ -269,7 +269,7 @@ namespace Microsoft.AspNetCore.Razor.Tasks
Assert.Equal(expectedError, message);
}
[Fact]
[Fact(Skip = "https://github.com/dotnet/aspnetcore/issues/24257")]
public void Fails_WhenStaticWebAsset_HaveDifferentContentRoot()
{
// Arrange

View File

@ -0,0 +1 @@
<p>Hello from razor</p>

View File

@ -0,0 +1,3 @@
p {
font-size: bold;
}

View File

@ -0,0 +1,3 @@
button {
font-size: 16px;
}

View File

@ -0,0 +1,3 @@
h1 {
font-weight: bold;
}

View File

@ -0,0 +1 @@
<p>Hello from razor</p>

View File

@ -0,0 +1,3 @@
p {
font-size: bold;
}