* 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:
parent
11835cf768
commit
b326be1710
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -246,6 +246,7 @@ Copyright (c) .NET Foundation. All rights reserved.
|
|||
|
||||
<GetCurrentProjectStaticWebAssetsDependsOn>
|
||||
$(GetCurrentProjectStaticWebAssetsDependsOn);
|
||||
AddScopedCssBundle;
|
||||
_BlazorWasmPrepareForRun;
|
||||
</GetCurrentProjectStaticWebAssetsDependsOn>
|
||||
</PropertyGroup>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>";
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)}");
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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'" />
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
<p>Hello from razor</p>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
p {
|
||||
font-size: bold;
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
button {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
h1 {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
<p>Hello from razor</p>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
p {
|
||||
font-size: bold;
|
||||
}
|
||||
Loading…
Reference in New Issue