Skip to content
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
7 changes: 7 additions & 0 deletions sdk.sln
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "dotnet-watch.Tests", "src\T
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Watch.BrowserRefresh.Tests", "src\Tests\Microsoft.AspNetCore.Watch.BrowserRefresh.Tests\Microsoft.AspNetCore.Watch.BrowserRefresh.Tests.csproj", "{81ADA3FA-AC26-4149-8CFC-EC7808ECB820}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotNetWatchTasks", "src\BuiltInTools\DotNetWatchTasks\DotNetWatchTasks.csproj", "{A41DF752-6F21-4036-AD02-DD37B11A2723}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -498,6 +500,10 @@ Global
{81ADA3FA-AC26-4149-8CFC-EC7808ECB820}.Debug|Any CPU.Build.0 = Debug|Any CPU
{81ADA3FA-AC26-4149-8CFC-EC7808ECB820}.Release|Any CPU.ActiveCfg = Release|Any CPU
{81ADA3FA-AC26-4149-8CFC-EC7808ECB820}.Release|Any CPU.Build.0 = Release|Any CPU
{A41DF752-6F21-4036-AD02-DD37B11A2723}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A41DF752-6F21-4036-AD02-DD37B11A2723}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A41DF752-6F21-4036-AD02-DD37B11A2723}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A41DF752-6F21-4036-AD02-DD37B11A2723}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -590,6 +596,7 @@ Global
{A82EF2B9-24BC-4569-8FE5-9EF51017F4CB} = {71A9F549-0EB6-41F9-BC16-4A6C5007FC91}
{CCE1A328-9CFE-44D3-B68F-FE84A039ACEA} = {580D1AE7-AA8F-4912-8B76-105594E00B3B}
{81ADA3FA-AC26-4149-8CFC-EC7808ECB820} = {580D1AE7-AA8F-4912-8B76-105594E00B3B}
{A41DF752-6F21-4036-AD02-DD37B11A2723} = {71A9F549-0EB6-41F9-BC16-4A6C5007FC91}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {FB8F26CE-4DE6-433F-B32A-79183020BBD6}
Expand Down
71 changes: 69 additions & 2 deletions src/BuiltInTools/BrowserRefresh/WebSocketScriptInjection.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,84 @@ setTimeout(function () {
console.debug(ex);
return;
}

let waiting = false;

connection.onmessage = function (message) {
if (message.data === 'Reload') {
console.debug('Server is ready. Reloading...');
location.reload();
} else if (message.data === 'Wait') {
if (waiting) {
return;
}
waiting = true;
console.debug('File changes detected. Waiting for application to rebuild.');
const t = document.title; const r = ['☱', '☲', '☴']; let i = 0;
setInterval(function () { document.title = r[i++ % r.length] + ' ' + t; }, 240);
const glyphs = ['☱', '☲', '☴'];
const title = document.title;
let i = 0;
setInterval(function () { document.title = glyphs[i++ % glyphs.length] + ' ' + title; }, 240);
} else {
const parsed = JSON.parse(message.data);
if (parsed.type == 'UpdateStaticFile') {
const path = parsed.path;
if (path && path.endsWith('.css')) {
updateCssByPath(path);
} else {
console.debug(`File change detected to css file ${path}. Reloading page...`);
location.reload();
return;
}
}
}
}

connection.onerror = function (event) { console.debug('dotnet-watch reload socket error.', event) }
connection.onclose = function () { console.debug('dotnet-watch reload socket closed.') }
connection.onopen = function () { console.debug('dotnet-watch reload socket connected.') }

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

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

updateCssElement(styleElement);
}

function updateAllLocalCss() {
[...document.querySelectorAll('link')]
Copy link
Member

Choose a reason for hiding this comment

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

Super nit: We might want to filter by rel='stylesheet' to avoid selecting links to non-stylesheet assets.

Probably an edge case so feel free to ignore.

.filter(l => l.baseURI === document.baseURI)
.forEach(e => updateCssElement(e));
}

function updateCssElement(styleElement) {
if (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;
}

const newElement = styleElement.cloneNode();
const href = styleElement.href;
newElement.href = href.split('?', 1)[0] + `?nonce=${Date.now()}`;
Copy link
Member

Choose a reason for hiding this comment

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

Is there a chance href.split('?', 1) can be empty?


styleElement.loading = true;
newElement.loading = true;
newElement.addEventListener('load', function () {
newElement.loading = false;
styleElement.remove();
});

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));
}
}, 500);
16 changes: 16 additions & 0 deletions src/BuiltInTools/DotNetWatchTasks/DotNetWatchTasks.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Build.Framework" Version="$(MicrosoftBuildFrameworkVersion)" ExcludeAssets="Runtime" />
<PackageReference Include="Microsoft.Build.Utilities.Core" Version="$(MicrosoftBuildUtilitiesCoreVersion)" ExcludeAssets="Runtime" />
</ItemGroup>

<ItemGroup>
<Compile Include="..\dotnet-watch\Internal\MSBuildFileSetResult.cs" />
</ItemGroup>

</Project>
71 changes: 71 additions & 0 deletions src/BuiltInTools/DotNetWatchTasks/FileSetSerializer.cs
Original file line number Diff line number Diff line change
@@ -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.Collections.Generic;
using System.IO;
using System.Runtime.Serialization.Json;
using System.Text;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using Microsoft.DotNet.Watcher.Internal;

namespace DotNetWatchTasks
{
public class FileSetSerializer : Task
{
public ITaskItem[] WatchFiles { get; set; }

public bool IsNetCoreApp31OrNewer { get; set; }
Copy link
Contributor

Choose a reason for hiding this comment

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

Reminder where this is used again?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We use it to determine if we can inject the browser refresh middleware. It compiles against netcoreapp3.1 so we have to make sure it's at least that


public ITaskItem OutputPath { get; set; }

public string[] PackageIds { get; set; }

public override bool Execute()
{
var projectItems = new Dictionary<string, ProjectItems>(StringComparer.OrdinalIgnoreCase);
var fileSetResult = new MSBuildFileSetResult
{
IsNetCoreApp31OrNewer = IsNetCoreApp31OrNewer,
Projects = projectItems
};

foreach (var item in WatchFiles)
{
var fullPath = item.GetMetadata("FullPath");
var staticWebAssetPath = item.GetMetadata("StaticWebAssetPath");
var projectFullPath = item.GetMetadata("ProjectFullPath");

if (!projectItems.TryGetValue(projectFullPath, out var project))
{
projectItems[projectFullPath] = project = new ProjectItems();
}

if (string.IsNullOrEmpty(staticWebAssetPath))
{
project.Files.Add(fullPath);
}
else
{
project.StaticFiles.Add(new StaticFileItem
{
FilePath = fullPath,
StaticWebAssetPath = staticWebAssetPath,
});
}
}

var serializer = new DataContractJsonSerializer(fileSetResult.GetType(), new DataContractJsonSerializerSettings
{
UseSimpleDictionaryFormat = true,
});

using var fileStream = File.Create(OutputPath.ItemSpec);
using var writer = JsonReaderWriterFactory.CreateJsonWriter(fileStream, Encoding.UTF8, ownsStream: false, indent: true);
serializer.WriteObject(writer, fileSetResult);

return !Log.HasLoggedErrors;
}
}
}
9 changes: 8 additions & 1 deletion src/BuiltInTools/dotnet-watch/BrowserRefreshServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System;
using System.Linq;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
Expand All @@ -20,6 +21,8 @@ namespace Microsoft.DotNet.Watcher.Tools
{
public class BrowserRefreshServer : IAsyncDisposable
{
private readonly byte[] ReloadMessage = Encoding.UTF8.GetBytes("Reload");
private readonly byte[] WaitMessage = Encoding.UTF8.GetBytes("Wait");
private readonly IReporter _reporter;
private readonly TaskCompletionSource _taskCompletionSource;
private IHost _refreshServer;
Expand Down Expand Up @@ -73,7 +76,7 @@ private async Task WebSocketRequest(HttpContext context)
await _taskCompletionSource.Task;
}

public async Task SendMessage(byte[] messageBytes, CancellationToken cancellationToken = default)
public async ValueTask SendMessage(ReadOnlyMemory<byte> messageBytes, CancellationToken cancellationToken = default)
{
if (_webSocket == null || _webSocket.CloseStatus.HasValue)
{
Expand Down Expand Up @@ -105,5 +108,9 @@ public async ValueTask DisposeAsync()

_taskCompletionSource.TrySetResult();
}

public ValueTask ReloadAsync(CancellationToken cancellationToken) => SendMessage(ReloadMessage, cancellationToken);

public ValueTask SendWaitMessageAsync(CancellationToken cancellationToken) => SendMessage(WaitMessage, cancellationToken);
}
}
32 changes: 23 additions & 9 deletions src/BuiltInTools/dotnet-watch/DotNetWatch.targets
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ Main target called by dotnet-watch. It gathers MSBuild items and writes
them to a file.
=========================================================================
-->
<UsingTask AssemblyFile="$(MSBuildThisFileDirectory)DotNetWatchTasks.dll" TaskName="FileSetSerializer" />

<Target Name="GenerateWatchList"
DependsOnTargets="_CollectWatchItems">

Expand All @@ -17,14 +19,10 @@ them to a file.
<_IsMicrosoftNETCoreApp31OrNewer Condition="'$(_IsMicrosoftNETCoreApp31OrNewer)' == ''">false</_IsMicrosoftNETCoreApp31OrNewer>
</PropertyGroup>

<ItemGroup>
<_WatchListLine Include="$(_IsMicrosoftNETCoreApp31OrNewer)" />
<_WatchListLine Include="%(Watch.FullPath)" />
</ItemGroup>

<WriteLinesToFile Overwrite="true"
File="$(_DotNetWatchListFile)"
Lines="@(_WatchListLine)" />
<FileSetSerializer
IsNetCoreApp31OrNewer="$(_IsMicrosoftNETCoreApp31OrNewer)"
OutputPath="$(_DotNetWatchListFile)"
WatchFiles="@(Watch)" />
</Target>

<!--
Expand All @@ -45,7 +43,11 @@ Returns: @(Watch)
</_CollectWatchItemsDependsOn>
</PropertyGroup>

<Target Name="_CollectWatchItems" DependsOnTargets="$(_CollectWatchItemsDependsOn)" Returns="@(Watch)" />
<Target Name="_CollectWatchItems" DependsOnTargets="$(_CollectWatchItemsDependsOn)" Returns="@(Watch)">
<ItemGroup>
<Watch ProjectFullPath="$(MSBuildProjectFullPath)" Condition="'%(Watch.ProjectFullPath)' == ''" />
</ItemGroup>
</Target>

<Target Name="_CollectWatchItemsPerFramework">
<ItemGroup>
Expand All @@ -65,10 +67,21 @@ Returns: @(Watch)

<Error Text="TargetFramework should be set" Condition="'$(TargetFramework)' == '' "/>

<PropertyGroup Condition="'$(_DotNetWatchUseStaticWebAssetBasePath)' == 'true'">
<_DotNetWatchStaticWebAssetBasePath Condition="'$(StaticWebAssetBasePath)' != ''">$(StaticWebAssetBasePath)/</_DotNetWatchStaticWebAssetBasePath>
<_DotNetWatchStaticWebAssetBasePath Condition="'$(StaticWebAssetBasePath)' == ''">_content/$(PackageId)/</_DotNetWatchStaticWebAssetBasePath>
</PropertyGroup>

<ItemGroup>
<Watch Include="%(Compile.FullPath)" Condition="'%(Compile.Watch)' != 'false'" />
<Watch Include="%(EmbeddedResource.FullPath)" Condition="'%(EmbeddedResource.Watch)' != 'false'"/>
<Watch Include="$(MSBuildProjectFullPath)" />

<!-- In RazorSDK (Blazor, RCL, and Web) targeting apps also watch content files under wwwroot -->
<Watch Include="%(Content.FullPath)"
Condition="'$(UsingMicrosoftNETSdkRazor)'=='true' AND '$(DotNetWatchContentFiles)'!='false' AND '%(Content.Watch)' != 'false' AND $([System.String]::Copy('%(Identity)').Replace('\','/').StartsWith('wwwroot/'))"
StaticWebAssetPath="$(_DotNetWatchStaticWebAssetBasePath)$([System.String]::Copy('%(Identity)').Replace('\','/').Substring(8))" />

<_WatchProjects Include="%(ProjectReference.Identity)" Condition="'%(ProjectReference.Watch)' != 'false'" />
</ItemGroup>

Expand All @@ -77,6 +90,7 @@ Returns: @(Watch)
BuildInParallel="true">
<Output TaskParameter="TargetOutputs" ItemName="Watch" />
</MSBuild>

</Target>

</Project>
6 changes: 4 additions & 2 deletions src/BuiltInTools/dotnet-watch/DotNetWatchContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,16 @@ public class DotNetWatchContext

public ProcessSpec ProcessSpec { get; set; }

public IFileSet FileSet { get; set; }
public FileSet FileSet { get; set; }
Copy link
Member

Choose a reason for hiding this comment

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

Why do we want to use the concrete type instead of the interface here?

Copy link
Member

Choose a reason for hiding this comment

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

Nevermind. I see that the interface was removed.

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's a POCO type, there's really no need for there to be an interface and a concrete implementation. Kinda gets in the way since you have to update two places every time you make a change to the contract here.


public int Iteration { get; set; }

public string ChangedFile { get; set; }
public FileItem? ChangedFile { get; set; }

public bool RequiresMSBuildRevaluation { get; set; }

public bool SuppressMSBuildIncrementalism { get; set; }

public BrowserRefreshServer BrowserRefreshServer { get; set; }
}
}
30 changes: 30 additions & 0 deletions src/BuiltInTools/dotnet-watch/DotNetWatchOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// 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;

namespace Microsoft.DotNet.Watcher
{
public record DotNetWatchOptions(
bool SuppressHandlingStaticContentFiles,
bool SuppressMSBuildIncrementalism,
bool SuppressLaunchBrowser,
bool SuppressBrowserRefresh,
bool RunningAsTest)
{
public static DotNetWatchOptions Default { get; } = new DotNetWatchOptions
(
SuppressHandlingStaticContentFiles: IsEnvironmentSet("DOTNET_WATCH_SUPPRESS_STATIC_FILE_HANDLING"),
SuppressMSBuildIncrementalism: IsEnvironmentSet("DOTNET_WATCH_SUPPRESS_MSBUILD_INCREMENTALISM"),
SuppressLaunchBrowser: IsEnvironmentSet("DOTNET_WATCH_SUPPRESS_LAUNCH_BROWSER"),
SuppressBrowserRefresh: IsEnvironmentSet("DOTNET_WATCH_SUPPRESS_BROWSER_REFRESH"),
RunningAsTest: IsEnvironmentSet("__DOTNET_WATCH_RUNNING_AS_TEST")
);

private static bool IsEnvironmentSet(string key)
{
var envValue = Environment.GetEnvironmentVariable(key);
return envValue == "1" || envValue == "true";
}
}
}
Loading