-
-
Notifications
You must be signed in to change notification settings - Fork 75
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: added css critical section generator as tool
- Loading branch information
1 parent
1e73727
commit 71b1b59
Showing
11 changed files
with
453 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
<?xml version="1.0" encoding="utf-8"?> | ||
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> | ||
<PropertyGroup Label="Analyzer settings"> | ||
<AnalysisMode>AllEnabledByDefault</AnalysisMode> | ||
<EnableNETAnalyzers>true</EnableNETAnalyzers> | ||
<AnalysisLevel>latest</AnalysisLevel> | ||
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild> | ||
<NuGetAuditLevel>critical</NuGetAuditLevel> | ||
<Nullable>enable</Nullable> | ||
</PropertyGroup> | ||
|
||
<ItemGroup Label="Code Analyzers"> | ||
<PackageReference Include="IDisposableAnalyzers" PrivateAssets="All" IncludeAssets="Runtime;Build;Native;contentFiles;Analyzers" /> | ||
</ItemGroup> | ||
|
||
<PropertyGroup Condition="'$(Configuration)' == 'Release'"> | ||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors> | ||
</PropertyGroup> | ||
|
||
</Project> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
using CommandLine; | ||
|
||
namespace LinkDotNet.Blog.CriticalCSS; | ||
|
||
public class CommandLineOptions | ||
{ | ||
[Option('i', "install-playwright", Required = false, | ||
HelpText = "Install Playwright dependencies")] | ||
public bool InstallPlaywright { get; init; } | ||
|
||
[Option('o', "output", Required = false, | ||
HelpText = "Output mode: console, file, or layout")] | ||
public string? OutputMode { get; init; } | ||
|
||
[Option('p', "path", Required = false, | ||
HelpText = "File path when using file or layout output mode")] | ||
public string? FilePath { get; init; } | ||
|
||
[Option('h', "help", Required = false, | ||
HelpText = "Show help information")] | ||
public bool Help { get; init; } | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
using Microsoft.Playwright; | ||
|
||
namespace LinkDotNet.Blog.CriticalCSS; | ||
|
||
internal static class CriticalCssGenerator | ||
{ | ||
public static async Task<string> GenerateAsync(IReadOnlyCollection<string>urls) | ||
{ | ||
using var playwright = await Playwright.CreateAsync(); | ||
await using var browser = await playwright.Chromium.LaunchAsync(); | ||
var criticalCss = new HashSet<string>(); | ||
|
||
var viewports = new[] | ||
{ | ||
new ViewportSize { Width = 1920, Height = 1080 }, | ||
new ViewportSize { Width = 375, Height = 667 } | ||
}; | ||
|
||
foreach (var viewport in viewports) | ||
{ | ||
foreach (var url in urls) | ||
{ | ||
var page = await browser.NewPageAsync(); | ||
await page.GotoAsync(url); | ||
await page.SetViewportSizeAsync(viewport.Width, viewport.Height); | ||
|
||
var usedCss = await page.EvaluateAsync<string[]>( | ||
""" | ||
() => { | ||
const styleSheets = Array.from(document.styleSheets); | ||
const usedRules = new Set(); | ||
const viewportHeight = window.innerHeight; | ||
const elements = document.querySelectorAll('*'); | ||
const aboveFold = Array.from(elements).filter(el => { | ||
const rect = el.getBoundingClientRect(); | ||
return rect.top < viewportHeight; | ||
}); | ||
styleSheets.forEach(sheet => { | ||
try { | ||
Array.from(sheet.cssRules).forEach(rule => { | ||
if (rule.type === 1) { | ||
aboveFold.forEach(el => { | ||
if (el.matches(rule.selectorText)) { | ||
usedRules.add(rule.cssText); | ||
} | ||
}); | ||
} | ||
}); | ||
} catch (e) { | ||
} | ||
}); | ||
return Array.from(usedRules); | ||
} | ||
"""); | ||
|
||
foreach (var css in usedCss) | ||
{ | ||
criticalCss.Add(css); | ||
} | ||
} | ||
} | ||
|
||
var criticalCssContent = string.Join(string.Empty, criticalCss).Replace("@", "@@", StringComparison.OrdinalIgnoreCase); | ||
var styleTag = $"<style>{criticalCssContent}</style>"; | ||
|
||
return styleTag; | ||
} | ||
} |
20 changes: 20 additions & 0 deletions
20
tools/LinkDotNet.Blog.CriticalCSS/LinkDotNet.Blog.CriticalCSS.csproj
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
|
||
<PropertyGroup> | ||
<OutputType>Exe</OutputType> | ||
<TargetFramework>net9.0</TargetFramework> | ||
<ImplicitUsings>enable</ImplicitUsings> | ||
<Nullable>enable</Nullable> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<ProjectReference Include="..\..\tests\LinkDotNet.Blog.TestUtilities\LinkDotNet.Blog.TestUtilities.csproj" /> | ||
</ItemGroup> | ||
|
||
<ItemGroup> | ||
<PackageReference Include="CommandLineParser" /> | ||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" /> | ||
<PackageReference Include="Microsoft.Playwright" /> | ||
</ItemGroup> | ||
|
||
</Project> |
56 changes: 56 additions & 0 deletions
56
tools/LinkDotNet.Blog.CriticalCSS/PlaywrightWebApplicationFactory.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
using LinkDotNet.Blog.Infrastructure.Persistence; | ||
using Microsoft.AspNetCore.Hosting; | ||
using Microsoft.AspNetCore.Hosting.Server; | ||
using Microsoft.AspNetCore.Hosting.Server.Features; | ||
using Microsoft.AspNetCore.Mvc.Testing; | ||
using Microsoft.Extensions.DependencyInjection; | ||
using Microsoft.Extensions.Hosting; | ||
|
||
namespace LinkDotNet.Blog.CriticalCSS; | ||
|
||
internal sealed class PlaywrightWebApplicationFactory : WebApplicationFactory<LinkDotNet.Blog.Web.Program> | ||
{ | ||
private IHost? host; | ||
|
||
public string ServerAddress => ClientOptions.BaseAddress.ToString(); | ||
|
||
public override IServiceProvider Services => host?.Services | ||
?? throw new InvalidOperationException("Create the Client first before retrieving instances from the container"); | ||
|
||
protected override IHost CreateHost(IHostBuilder builder) | ||
{ | ||
var testHost = builder.Build(); | ||
|
||
builder = builder.ConfigureWebHost(b => | ||
{ | ||
b.UseSetting("PersistenceProvider", PersistenceProvider.Sqlite.Key); | ||
b.UseSetting("ConnectionString", "DataSource=file::memory:?cache=shared"); | ||
b.UseSetting("Logging:LogLevel:Default", "Error"); | ||
b.UseKestrel(); | ||
}); | ||
|
||
host?.Dispose(); | ||
host = builder.Build(); | ||
host.Start(); | ||
|
||
var server = host!.Services.GetRequiredService<IServer>(); | ||
var addresses = server.Features.Get<IServerAddressesFeature>(); | ||
|
||
ClientOptions.BaseAddress = addresses!.Addresses | ||
.Select(x => new Uri(x)) | ||
.Last(); | ||
|
||
testHost.Start(); | ||
return testHost; | ||
} | ||
|
||
protected override void Dispose(bool disposing) | ||
{ | ||
if (disposing) | ||
{ | ||
host?.Dispose(); | ||
} | ||
|
||
base.Dispose(disposing); | ||
} | ||
} |
Oops, something went wrong.