From 12fc111791d883dcec6739df8ce3cf9889252928 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Wed, 24 Mar 2021 17:41:34 -0700 Subject: [PATCH 1/2] Add support for updating scoped css files --- .../BrowserRefresh/HostingFilter.cs | 8 +- .../WebSocketScriptInjection.js | 15 ++-- .../dotnet-watch/HotReload/HotReload.cs | 20 ++--- .../HotReload/ScopedCssFileHandler.cs | 73 +++++++++++++++++++ .../dotnet-watch/HotReloadDotNetWatcher.cs | 2 +- 5 files changed, 95 insertions(+), 23 deletions(-) create mode 100644 src/BuiltInTools/dotnet-watch/HotReload/ScopedCssFileHandler.cs diff --git a/src/BuiltInTools/BrowserRefresh/HostingFilter.cs b/src/BuiltInTools/BrowserRefresh/HostingFilter.cs index 68f32444b707..bd66f0dcb6e0 100644 --- a/src/BuiltInTools/BrowserRefresh/HostingFilter.cs +++ b/src/BuiltInTools/BrowserRefresh/HostingFilter.cs @@ -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; @@ -23,6 +23,12 @@ public Action Configure(Action next) { return app => { + app.Map("/_framework/clear-browser-cache", app1 => app1.Run(context => + { + // 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()); app.UseMiddleware(); next(app); diff --git a/src/BuiltInTools/BrowserRefresh/WebSocketScriptInjection.js b/src/BuiltInTools/BrowserRefresh/WebSocketScriptInjection.js index ec9e28143d7b..8a39b2cb55ed 100644 --- a/src/BuiltInTools/BrowserRefresh/WebSocketScriptInjection.js +++ b/src/BuiltInTools/BrowserRefresh/WebSocketScriptInjection.js @@ -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'); + 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(); } @@ -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; @@ -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)); } diff --git a/src/BuiltInTools/dotnet-watch/HotReload/HotReload.cs b/src/BuiltInTools/dotnet-watch/HotReload/HotReload.cs index f10905774911..b5d5f4f43e2b 100644 --- a/src/BuiltInTools/dotnet-watch/HotReload/HotReload.cs +++ b/src/BuiltInTools/dotnet-watch/HotReload/HotReload.cs @@ -4,6 +4,7 @@ using System; using System.Threading; using System.Threading.Tasks; +using Microsoft.DotNet.Watcher.Internal; using Microsoft.Extensions.Tools.Internal; namespace Microsoft.DotNet.Watcher.Tools @@ -11,11 +12,13 @@ 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); } @@ -26,17 +29,10 @@ public async ValueTask InitializeAsync(DotNetWatchContext dotNetWatchContext, Ca public async ValueTask 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() diff --git a/src/BuiltInTools/dotnet-watch/HotReload/ScopedCssFileHandler.cs b/src/BuiltInTools/dotnet-watch/HotReload/ScopedCssFileHandler.cs new file mode 100644 index 000000000000..d5f72cca3674 --- /dev/null +++ b/src/BuiltInTools/dotnet-watch/HotReload/ScopedCssFileHandler.cs @@ -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 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 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. + // 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; } + } + } +} diff --git a/src/BuiltInTools/dotnet-watch/HotReloadDotNetWatcher.cs b/src/BuiltInTools/dotnet-watch/HotReloadDotNetWatcher.cs index 966cfc90ec31..34226a8e0e00 100644 --- a/src/BuiltInTools/dotnet-watch/HotReloadDotNetWatcher.cs +++ b/src/BuiltInTools/dotnet-watch/HotReloadDotNetWatcher.cs @@ -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); From 8c9ba2c883820cd05837c21840887121f24c6333 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Wed, 24 Mar 2021 20:08:23 -0700 Subject: [PATCH 2/2] Update src/BuiltInTools/BrowserRefresh/HostingFilter.cs --- src/BuiltInTools/BrowserRefresh/HostingFilter.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/BuiltInTools/BrowserRefresh/HostingFilter.cs b/src/BuiltInTools/BrowserRefresh/HostingFilter.cs index bd66f0dcb6e0..1793eb03db75 100644 --- a/src/BuiltInTools/BrowserRefresh/HostingFilter.cs +++ b/src/BuiltInTools/BrowserRefresh/HostingFilter.cs @@ -25,6 +25,7 @@ public Action Configure(Action next) { app.Map("/_framework/clear-browser-cache", app1 => app1.Run(context => { + // 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;