Skip to content

Add support for updating scoped css files #16507

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 2 commits into from
Mar 25, 2021
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
9 changes: 8 additions & 1 deletion src/BuiltInTools/BrowserRefresh/HostingFilter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Globalization;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
Expand All @@ -23,6 +23,13 @@ public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
{
return app =>
{
app.Map("/_framework/clear-browser-cache", app1 => app1.Run(context =>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume this behavior will be sticking around for a while. If so, might help to add a comment on why we do this here (AKA move the comment below into the code).

{
// Scoped css files can contain links to other css files. We'll try clearing out the http caches to force the browser to re-download.
// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Clear-Site-Data#directives
context.Response.Headers["Clear-site-data"] = "\"cache\"";
return Task.CompletedTask;
}));
app.Map(WebSocketScriptInjection.WebSocketScriptUrl, app1 => app1.UseMiddleware<BrowserScriptMiddleware>());
app.UseMiddleware<BrowserRefreshMiddleware>();
next(app);
Expand Down
15 changes: 6 additions & 9 deletions src/BuiltInTools/BrowserRefresh/WebSocketScriptInjection.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,15 @@ setTimeout(function () {
}
}

function updateCssByPath(path) {
async function updateCssByPath(path) {
const styleElement = document.querySelector(`link[href^="${path}"]`) ||
document.querySelector(`link[href^="${document.baseURI}${path}"]`);

// Receive a Clear-site-data header.
await fetch('/_framework/clear-browser-cache');
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Scoped css files can contain links to other css files. We'll try clearing out the http caches to force the browser to re-download.


if (!styleElement || !styleElement.parentNode) {
console.debug('Unable to find a stylesheet to update. Updating all css.');
console.debug('Unable to find a stylesheet to update. Updating all local css files.');
updateAllLocalCss();
}

Expand All @@ -74,7 +77,7 @@ setTimeout(function () {
}

function updateCssElement(styleElement) {
if (styleElement.loading) {
if (!styleElement || styleElement.loading) {
// A file change notification may be triggered for the same file before the browser
// finishes processing a previous update. In this case, it's easiest to ignore later updates
return;
Expand All @@ -94,12 +97,6 @@ setTimeout(function () {
styleElement.parentNode.insertBefore(newElement, styleElement.nextSibling);
}

function updateScopedCss() {
[...document.querySelectorAll('link')]
.filter(l => l.baseURI === document.baseURI && l.href && l.href.indexOf('.styles.css') !== -1)
.forEach(e => updateCssElement(e));
}

function applyBlazorDeltas(deltas) {
deltas.forEach(d => window.Blazor._internal.applyHotReload(d.moduleId, d.metadataDelta, d.ilDelta));
}
Expand Down
20 changes: 8 additions & 12 deletions src/BuiltInTools/dotnet-watch/HotReload/HotReload.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,21 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.DotNet.Watcher.Internal;
using Microsoft.Extensions.Tools.Internal;

namespace Microsoft.DotNet.Watcher.Tools
{
internal class HotReload : IDisposable
{
private readonly StaticFileHandler _staticFileHandler;
private readonly ScopedCssFileHandler _scopedCssFileHandler;
private readonly CompilationHandler _compilationHandler;

public HotReload(IReporter reporter)
public HotReload(ProcessRunner processRunner, IReporter reporter)
{
_staticFileHandler = new StaticFileHandler(reporter);
_scopedCssFileHandler = new ScopedCssFileHandler(processRunner, reporter);
_compilationHandler = new CompilationHandler(reporter);
}

Expand All @@ -26,17 +29,10 @@ public async ValueTask InitializeAsync(DotNetWatchContext dotNetWatchContext, Ca

public async ValueTask<bool> TryHandleFileChange(DotNetWatchContext context, FileItem file, CancellationToken cancellationToken)
{
if (await _staticFileHandler.TryHandleFileChange(context, file, cancellationToken))
{
return true;
}

if (await _compilationHandler.TryHandleFileChange(context, file, cancellationToken)) // This needs to be 6.0
{
return true;
}

return false;
return
await _staticFileHandler.TryHandleFileChange(context, file, cancellationToken) ||
await _scopedCssFileHandler.TryHandleFileChange(context, file, cancellationToken) ||
await _compilationHandler.TryHandleFileChange(context, file, cancellationToken);
}

public void Dispose()
Expand Down
73 changes: 73 additions & 0 deletions src/BuiltInTools/dotnet-watch/HotReload/ScopedCssFileHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// 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.Threading;
using System.Threading.Tasks;
using Microsoft.DotNet.Cli.Utils;
using Microsoft.DotNet.Watcher.Internal;

namespace Microsoft.DotNet.Watcher.Tools
{
internal sealed class ScopedCssFileHandler
{
private static readonly string _muxerPath = new Muxer().MuxerPath;
private readonly ProcessRunner _processRunner;
private readonly Extensions.Tools.Internal.IReporter _reporter;

public ScopedCssFileHandler(ProcessRunner processRunner, Extensions.Tools.Internal.IReporter reporter)
{
_processRunner = processRunner;
_reporter = reporter;
}

public async ValueTask<bool> TryHandleFileChange(DotNetWatchContext context, FileItem file, CancellationToken cancellationToken)
{
if (!file.FilePath.EndsWith(".razor.css", StringComparison.Ordinal) &&
!file.FilePath.EndsWith(".cshtml.css", StringComparison.Ordinal))
{
return default;
}

_reporter.Verbose($"Handling file change event for scoped css file {file.FilePath}.");
if (!await RebuildScopedCss(file.ProjectPath, cancellationToken))
{
return false;
}
await HandleBrowserRefresh(context.BrowserRefreshServer, file, cancellationToken);
return true;
}

private async ValueTask<bool> RebuildScopedCss(string projectPath, CancellationToken cancellationToken)
{
var build = new ProcessSpec
{
Executable = _muxerPath,
Arguments = new[] { "msbuild", "/nologo", "/t:_PrepareForScopedCss", projectPath, }
};

var result = await _processRunner.RunAsync(build, cancellationToken);
return result == 0;
}

private static async Task HandleBrowserRefresh(BrowserRefreshServer browserRefreshServer, FileItem fileItem, CancellationToken cancellationToken)
{
// We'd like an accurate scoped css path, but this needs a lot of work to wire-up now.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does moving the CSS generation to source generators make things any easier?

I'm not super familiar with the EnC/source generators side of the house but wonder if that helps here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would remove the need to run msbuild and make it faster. But we would still need to know what the path of the css file would be in markup. It's a bit tricky because it's relative to the wwwroot of the project being served. For instance, in a hosted app, the file turns out to be MyApp.Client.styles.css even though the project being run is MyApp.Hosted. But if the bundle were present in the Shared folder, it's path would be _content/MyApp.Shared/MyApp.Shared.styles.css. Lots of oddities to work thru, so taking the easy path and updating everything makes it cheap.

// We'll handle this as part of https://github.com/dotnet/aspnetcore/issues/31217.
// For now, we'll make it look like some css file which would cause JS to update a
// single file if it's from the current project, or all locally hosted css files if it's a file from
// referenced project.
var cssFilePath = Path.GetFileNameWithoutExtension(fileItem.ProjectPath) + ".css";
var message = new UpdateStaticFileMessage { Path = cssFilePath };
await browserRefreshServer.SendJsonSerlialized(message, cancellationToken);
}

private readonly struct UpdateStaticFileMessage
{
public string Type => "UpdateStaticFile";

public string Path { get; init; }
}
}
}
2 changes: 1 addition & 1 deletion src/BuiltInTools/dotnet-watch/HotReloadDotNetWatcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ public async Task WatchAsync(DotNetWatchContext context, CancellationToken cance
using var fileSetWatcher = new FileSetWatcher(fileSet, _reporter);
try
{
using var hotReload = new HotReload(_reporter);
using var hotReload = new HotReload(_processRunner, _reporter);
await hotReload.InitializeAsync(context, cancellationToken);

var processTask = _processRunner.RunAsync(processSpec, combinedCancellationSource.Token);
Expand Down