Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 24, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ public async Task BuildMinimal_Works()
// 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);
Expand All @@ -35,6 +37,7 @@ public async Task BuildMinimal_Works()

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]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,58 @@ public async Task Publish_WithDefaultSettings_Works()
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()
{
Expand Down Expand Up @@ -578,6 +630,58 @@ public async Task Publish_HostedApp_VisualStudio()
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]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,7 @@ Copyright (c) .NET Foundation. All rights reserved.

<GetCurrentProjectStaticWebAssetsDependsOn>
$(GetCurrentProjectStaticWebAssetsDependsOn);
AddScopedCssBundle;
_BlazorWasmPrepareForRun;
</GetCurrentProjectStaticWebAssetsDependsOn>
</PropertyGroup>
Expand Down
4 changes: 2 additions & 2 deletions src/Razor/Microsoft.AspNetCore.Razor.Tools/src/Application.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -113,4 +113,4 @@ private static string[] ExpandResponseFiles(string[] args)
return expandedArgs.ToArray();
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -320,6 +320,9 @@ public async Task IncrementalBuild_WithP2P_WorksWhenBuildProjectReferencesIsDisa
[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");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,7 @@ public async Task Pack_NoBuild_IncludesStaticWebAssets()
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"),
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
}
}
Loading