diff --git a/AspNetCore.sln b/AspNetCore.sln
index bd7fc769c8..d5fda15eba 100644
--- a/AspNetCore.sln
+++ b/AspNetCore.sln
@@ -1445,6 +1445,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.NET.Sdk.BlazorWeb
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.NET.Sdk.BlazorWebAssembly.Tools", "src\Components\WebAssembly\Sdk\tools\Microsoft.NET.Sdk.BlazorWebAssembly.Tools.csproj", "{175E5CD8-92D4-46BB-882E-3A930D3302D4}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Watch.BrowserRefresh", "src\Tools\dotnet-watch\BrowserRefresh\src\Microsoft.AspNetCore.Watch.BrowserRefresh.csproj", "{A5CE25E9-89E1-4F2C-9B89-0C161707E700}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Watch.BrowserRefresh.Tests", "src\Tools\dotnet-watch\BrowserRefresh\test\Microsoft.AspNetCore.Watch.BrowserRefresh.Tests.csproj", "{E6A23627-8D63-4DF1-A4F2-8881172C1FE6}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -6843,6 +6847,30 @@ Global
{175E5CD8-92D4-46BB-882E-3A930D3302D4}.Release|x64.Build.0 = Release|Any CPU
{175E5CD8-92D4-46BB-882E-3A930D3302D4}.Release|x86.ActiveCfg = Release|Any CPU
{175E5CD8-92D4-46BB-882E-3A930D3302D4}.Release|x86.Build.0 = Release|Any CPU
+ {A5CE25E9-89E1-4F2C-9B89-0C161707E700}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A5CE25E9-89E1-4F2C-9B89-0C161707E700}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A5CE25E9-89E1-4F2C-9B89-0C161707E700}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {A5CE25E9-89E1-4F2C-9B89-0C161707E700}.Debug|x64.Build.0 = Debug|Any CPU
+ {A5CE25E9-89E1-4F2C-9B89-0C161707E700}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {A5CE25E9-89E1-4F2C-9B89-0C161707E700}.Debug|x86.Build.0 = Debug|Any CPU
+ {A5CE25E9-89E1-4F2C-9B89-0C161707E700}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A5CE25E9-89E1-4F2C-9B89-0C161707E700}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A5CE25E9-89E1-4F2C-9B89-0C161707E700}.Release|x64.ActiveCfg = Release|Any CPU
+ {A5CE25E9-89E1-4F2C-9B89-0C161707E700}.Release|x64.Build.0 = Release|Any CPU
+ {A5CE25E9-89E1-4F2C-9B89-0C161707E700}.Release|x86.ActiveCfg = Release|Any CPU
+ {A5CE25E9-89E1-4F2C-9B89-0C161707E700}.Release|x86.Build.0 = Release|Any CPU
+ {E6A23627-8D63-4DF1-A4F2-8881172C1FE6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E6A23627-8D63-4DF1-A4F2-8881172C1FE6}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E6A23627-8D63-4DF1-A4F2-8881172C1FE6}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {E6A23627-8D63-4DF1-A4F2-8881172C1FE6}.Debug|x64.Build.0 = Debug|Any CPU
+ {E6A23627-8D63-4DF1-A4F2-8881172C1FE6}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {E6A23627-8D63-4DF1-A4F2-8881172C1FE6}.Debug|x86.Build.0 = Debug|Any CPU
+ {E6A23627-8D63-4DF1-A4F2-8881172C1FE6}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E6A23627-8D63-4DF1-A4F2-8881172C1FE6}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E6A23627-8D63-4DF1-A4F2-8881172C1FE6}.Release|x64.ActiveCfg = Release|Any CPU
+ {E6A23627-8D63-4DF1-A4F2-8881172C1FE6}.Release|x64.Build.0 = Release|Any CPU
+ {E6A23627-8D63-4DF1-A4F2-8881172C1FE6}.Release|x86.ActiveCfg = Release|Any CPU
+ {E6A23627-8D63-4DF1-A4F2-8881172C1FE6}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -7567,6 +7595,8 @@ Global
{83371889-9A3E-4D16-AE77-EB4F83BC6374} = {FED4267E-E5E4-49C5-98DB-8B3F203596EE}
{525EBCB4-A870-470B-BC90-845306C337D1} = {FED4267E-E5E4-49C5-98DB-8B3F203596EE}
{175E5CD8-92D4-46BB-882E-3A930D3302D4} = {FED4267E-E5E4-49C5-98DB-8B3F203596EE}
+ {A5CE25E9-89E1-4F2C-9B89-0C161707E700} = {B6118E15-C37A-4B05-B4DF-97FE99790417}
+ {E6A23627-8D63-4DF1-A4F2-8881172C1FE6} = {B6118E15-C37A-4B05-B4DF-97FE99790417}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {3E8720B3-DBDD-498C-B383-2CC32A054E8F}
diff --git a/Directory.Build.targets b/Directory.Build.targets
index a0de4f05b6..d4a4e9a2a0 100644
--- a/Directory.Build.targets
+++ b/Directory.Build.targets
@@ -147,7 +147,7 @@
-
+
$(SharedFxVersion)
$(SharedFxVersion)
$(SharedFxVersion)
diff --git a/THIRD-PARTY-NOTICES.txt b/THIRD-PARTY-NOTICES.txt
index 99d1e37721..45f0a37b30 100644
--- a/THIRD-PARTY-NOTICES.txt
+++ b/THIRD-PARTY-NOTICES.txt
@@ -217,3 +217,54 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+
+License notice for West Wind Live Reload ASP.NET Core Middleware
+=============================================
+
+
+MIT License
+-----------
+
+Copyright (c) 2019-2020 West Wind Technologies
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+License notice for cli-spinners
+=============================================
+
+MIT License
+
+Copyright (c) Sindre Sorhus (https://sindresorhus.com)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
\ No newline at end of file
diff --git a/eng/ProjectReferences.props b/eng/ProjectReferences.props
index 6dbfca4646..e39540d453 100644
--- a/eng/ProjectReferences.props
+++ b/eng/ProjectReferences.props
@@ -66,6 +66,7 @@
+
diff --git a/eng/Workarounds.targets b/eng/Workarounds.targets
index 4a00c1786b..a2cc87f1a0 100644
--- a/eng/Workarounds.targets
+++ b/eng/Workarounds.targets
@@ -20,7 +20,7 @@
The web sdk adds an implicit framework reference. This removes it until we can update our build to use framework references.
-->
-
+
diff --git a/src/Tools/Tools.slnf b/src/Tools/Tools.slnf
index 813a7aa908..7e3d29f08b 100644
--- a/src/Tools/Tools.slnf
+++ b/src/Tools/Tools.slnf
@@ -17,7 +17,11 @@
"src\\Tools\\dotnet-user-secrets\\src\\dotnet-user-secrets.csproj",
"src\\Tools\\dotnet-user-secrets\\test\\dotnet-user-secrets.Tests.csproj",
"src\\Tools\\dotnet-watch\\src\\dotnet-watch.csproj",
- "src\\Tools\\dotnet-watch\\test\\dotnet-watch.Tests.csproj"
+ "src\\Tools\\dotnet-watch\\BrowserRefresh\\src\\Microsoft.AspNetCore.Watch.BrowserRefresh.csproj",
+ "src\\Tools\\dotnet-watch\\BrowserRefresh\\test\\Microsoft.AspNetCore.Watch.BrowserRefresh.Tests.csproj",
+ "src\\Tools\\dotnet-watch\\test\\dotnet-watch.Tests.csproj",
+ "src\\Middleware\\StaticFiles\\src\\Microsoft.AspNetCore.StaticFiles.csproj",
+ "src\\Hosting\\TestHost\\src\\Microsoft.AspNetCore.TestHost.csproj"
]
}
}
\ No newline at end of file
diff --git a/src/Tools/dotnet-watch/BrowserRefresh/src/BrowserRefreshMiddleware.cs b/src/Tools/dotnet-watch/BrowserRefresh/src/BrowserRefreshMiddleware.cs
new file mode 100644
index 0000000000..210b4f73e8
--- /dev/null
+++ b/src/Tools/dotnet-watch/BrowserRefresh/src/BrowserRefreshMiddleware.cs
@@ -0,0 +1,107 @@
+// 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.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Features;
+using Microsoft.Extensions.Logging;
+using Microsoft.Net.Http.Headers;
+
+namespace Microsoft.AspNetCore.Watch.BrowserRefresh
+{
+ public class BrowserRefreshMiddleware
+ {
+ private static readonly MediaTypeHeaderValue _textHtmlMediaType = new MediaTypeHeaderValue("text/html");
+ private readonly RequestDelegate _next;
+ private readonly ILogger _logger;
+
+ public BrowserRefreshMiddleware(RequestDelegate next, ILogger logger) =>
+ (_next, _logger) = (next, logger);
+
+ public async Task InvokeAsync(HttpContext context)
+ {
+ // We only need to support this for requests that could be initiated by a browser.
+ if (IsBrowserRequest(context))
+ {
+ // Use a custom StreamWrapper to rewrite output on Write/WriteAsync
+ using var responseStreamWrapper = new ResponseStreamWrapper(context, _logger);
+ var originalBodyFeature = context.Features.Get();
+ context.Features.Set(new StreamResponseBodyFeature(responseStreamWrapper));
+
+ try
+ {
+ await _next(context);
+ }
+ finally
+ {
+ context.Features.Set(originalBodyFeature);
+ }
+
+ if (responseStreamWrapper.IsHtmlResponse && _logger.IsEnabled(LogLevel.Debug))
+ {
+ if (responseStreamWrapper.ScriptInjectionPerformed)
+ {
+ Log.BrowserConfiguredForRefreshes(_logger);
+ }
+ else
+ {
+ Log.FailedToConfiguredForRefreshes(_logger);
+ }
+ }
+ }
+ else
+ {
+ await _next(context);
+ }
+ }
+
+ internal static bool IsBrowserRequest(HttpContext context)
+ {
+ var request = context.Request;
+ if (!HttpMethods.IsGet(request.Method) && !HttpMethods.IsPost(request.Method))
+ {
+ return false;
+ }
+
+ var typedHeaders = request.GetTypedHeaders();
+ if (!(typedHeaders.Accept is IList acceptHeaders))
+ {
+ return false;
+ }
+
+ for (var i = 0; i < acceptHeaders.Count; i++)
+ {
+ if (acceptHeaders[i].IsSubsetOf(_textHtmlMediaType))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ internal static class Log
+ {
+ private static readonly Action _setupResponseForBrowserRefresh = LoggerMessage.Define(
+ LogLevel.Debug,
+ new EventId(1, "SetUpResponseForBrowserRefresh"),
+ "Response markup is scheduled to include browser refresh script injection.");
+
+ private static readonly Action _browserConfiguredForRefreshes = LoggerMessage.Define(
+ LogLevel.Debug,
+ new EventId(2, "BrowserConfiguredForRefreshes"),
+ "Response markup was updated to include browser refresh script injection.");
+
+ private static readonly Action _failedToConfigureForRefreshes = LoggerMessage.Define(
+ LogLevel.Debug,
+ new EventId(3, "FailedToConfiguredForRefreshes"),
+ "Unable to configure browser refresh script injection on the response.");
+
+ public static void SetupResponseForBrowserRefresh(ILogger logger) => _setupResponseForBrowserRefresh(logger, null);
+ public static void BrowserConfiguredForRefreshes(ILogger logger) => _browserConfiguredForRefreshes(logger, null);
+ public static void FailedToConfiguredForRefreshes(ILogger logger) => _failedToConfigureForRefreshes(logger, null);
+ }
+ }
+}
diff --git a/src/Tools/dotnet-watch/BrowserRefresh/src/HostingFilter.cs b/src/Tools/dotnet-watch/BrowserRefresh/src/HostingFilter.cs
new file mode 100644
index 0000000000..02135dca0d
--- /dev/null
+++ b/src/Tools/dotnet-watch/BrowserRefresh/src/HostingFilter.cs
@@ -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;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+
+[assembly: HostingStartup(typeof(Microsoft.AspNetCore.Watch.BrowserRefresh.HostingStartup))]
+
+namespace Microsoft.AspNetCore.Watch.BrowserRefresh
+{
+ internal sealed class HostingStartup : IHostingStartup, IStartupFilter
+ {
+ public void Configure(IWebHostBuilder builder)
+ {
+ builder.ConfigureServices(services => services.TryAddEnumerable(ServiceDescriptor.Singleton(this)));
+ }
+
+ public Action Configure(Action next)
+ {
+ return app =>
+ {
+ app.UseMiddleware();
+ next(app);
+ };
+ }
+ }
+}
diff --git a/src/Tools/dotnet-watch/BrowserRefresh/src/Microsoft.AspNetCore.Watch.BrowserRefresh.csproj b/src/Tools/dotnet-watch/BrowserRefresh/src/Microsoft.AspNetCore.Watch.BrowserRefresh.csproj
new file mode 100644
index 0000000000..017eb2028c
--- /dev/null
+++ b/src/Tools/dotnet-watch/BrowserRefresh/src/Microsoft.AspNetCore.Watch.BrowserRefresh.csproj
@@ -0,0 +1,18 @@
+
+
+
+ netcoreapp3.1
+ false
+ enable
+ false
+ true
+ true
+ false
+
+
+
+
+
+
+
+
diff --git a/src/Tools/dotnet-watch/BrowserRefresh/src/ResponseStreamWrapper.cs b/src/Tools/dotnet-watch/BrowserRefresh/src/ResponseStreamWrapper.cs
new file mode 100644
index 0000000000..dfc5d46503
--- /dev/null
+++ b/src/Tools/dotnet-watch/BrowserRefresh/src/ResponseStreamWrapper.cs
@@ -0,0 +1,153 @@
+// 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.
+
+// Based on https://github.com/RickStrahl/Westwind.AspnetCore.LiveReload/blob/128b5f524e86954e997f2c453e7e5c1dcc3db746/Westwind.AspnetCore.LiveReload/ResponseStreamWrapper.cs
+
+using System;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Logging;
+using Microsoft.Net.Http.Headers;
+
+namespace Microsoft.AspNetCore.Watch.BrowserRefresh
+{
+ ///
+ /// Wraps the Response Stream to inject the WebSocket HTML into
+ /// an HTML Page.
+ ///
+ public class ResponseStreamWrapper : Stream
+ {
+ private static readonly MediaTypeHeaderValue _textHtmlMediaType = new MediaTypeHeaderValue("text/html");
+ private readonly Stream _baseStream;
+ private readonly HttpContext _context;
+ private readonly ILogger _logger;
+ private bool? _isHtmlResponse;
+
+ public ResponseStreamWrapper(HttpContext context, ILogger logger)
+ {
+ _context = context;
+ _baseStream = context.Response.Body;
+ _logger = logger;
+ }
+
+ public override bool CanRead => false;
+ public override bool CanSeek => false;
+ public override bool CanWrite => true;
+ public override long Length { get; }
+ public override long Position { get; set; }
+ public bool ScriptInjectionPerformed { get; private set; }
+
+ public bool IsHtmlResponse => _isHtmlResponse ?? false;
+
+ public override void Flush()
+ {
+ OnWrite();
+ _baseStream.Flush();
+ }
+
+ public override Task FlushAsync(CancellationToken cancellationToken)
+ {
+ OnWrite();
+ return _baseStream.FlushAsync(cancellationToken);
+ }
+
+ public override void Write(ReadOnlySpan buffer)
+ {
+ OnWrite();
+ if (IsHtmlResponse && !ScriptInjectionPerformed)
+ {
+ ScriptInjectionPerformed = WebSocketScriptInjection.Instance.TryInjectLiveReloadScript(_baseStream, buffer);
+ }
+ else
+ {
+ _baseStream.Write(buffer);
+ }
+ }
+
+ public override void WriteByte(byte value)
+ {
+ OnWrite();
+ _baseStream.WriteByte(value);
+ }
+
+ public override void Write(byte[] buffer, int offset, int count)
+ {
+ OnWrite();
+
+ if (IsHtmlResponse && !ScriptInjectionPerformed)
+ {
+ ScriptInjectionPerformed = WebSocketScriptInjection.Instance.TryInjectLiveReloadScript(_baseStream, buffer.AsSpan(offset, count));
+ }
+ else
+ {
+ _baseStream.Write(buffer, offset, count);
+ }
+ }
+
+ public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
+ {
+ OnWrite();
+
+ if (IsHtmlResponse && !ScriptInjectionPerformed)
+ {
+ ScriptInjectionPerformed = await WebSocketScriptInjection.Instance.TryInjectLiveReloadScriptAsync(_baseStream, buffer.AsMemory(offset, count), cancellationToken);
+ }
+ else
+ {
+ await _baseStream.WriteAsync(buffer, offset, count, cancellationToken);
+ }
+ }
+
+ public override async ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default)
+ {
+ OnWrite();
+
+ if (IsHtmlResponse && !ScriptInjectionPerformed)
+ {
+ ScriptInjectionPerformed = await WebSocketScriptInjection.Instance.TryInjectLiveReloadScriptAsync(_baseStream, buffer, cancellationToken);
+ }
+ else
+ {
+ await _baseStream.WriteAsync(buffer, cancellationToken);
+ }
+ }
+
+ private void OnWrite()
+ {
+ if (_isHtmlResponse.HasValue)
+ {
+ return;
+ }
+
+ var response = _context.Response;
+
+ _isHtmlResponse =
+ (response.StatusCode == StatusCodes.Status200OK || response.StatusCode == StatusCodes.Status500InternalServerError) &&
+ MediaTypeHeaderValue.TryParse(response.ContentType, out var mediaType) &&
+ mediaType.IsSubsetOf(_textHtmlMediaType) &&
+ (!mediaType.Charset.HasValue || mediaType.Charset.Equals("utf-8", StringComparison.OrdinalIgnoreCase));
+
+ if (_isHtmlResponse.Value)
+ {
+ BrowserRefreshMiddleware.Log.SetupResponseForBrowserRefresh(_logger);
+
+ // Since we're changing the markup content, reset the content-length
+ response.Headers.ContentLength = null;
+ }
+ }
+
+ public override int Read(byte[] buffer, int offset, int count) => throw new NotSupportedException();
+
+ public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
+
+ public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
+ => throw new NotSupportedException();
+
+ public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default)
+ => throw new NotSupportedException();
+
+ public override void SetLength(long value) => throw new NotSupportedException();
+ }
+}
diff --git a/src/Tools/dotnet-watch/BrowserRefresh/src/StartupHook.cs b/src/Tools/dotnet-watch/BrowserRefresh/src/StartupHook.cs
new file mode 100644
index 0000000000..f0dbc50e61
--- /dev/null
+++ b/src/Tools/dotnet-watch/BrowserRefresh/src/StartupHook.cs
@@ -0,0 +1,10 @@
+// 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.
+
+internal class StartupHook
+{
+ public static void Initialize()
+ {
+ // This method exists to make startup hook load successfully. We do not need to do anything interesting here.
+ }
+}
diff --git a/src/Tools/dotnet-watch/BrowserRefresh/src/WebSocketScriptInjection.cs b/src/Tools/dotnet-watch/BrowserRefresh/src/WebSocketScriptInjection.cs
new file mode 100644
index 0000000000..f0e85e2fa8
--- /dev/null
+++ b/src/Tools/dotnet-watch/BrowserRefresh/src/WebSocketScriptInjection.cs
@@ -0,0 +1,86 @@
+// 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.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Microsoft.AspNetCore.Watch.BrowserRefresh
+{
+ ///
+ /// Helper class that handles the HTML injection into
+ /// a string or byte array.
+ ///
+ public class WebSocketScriptInjection
+ {
+ private const string BodyMarker = "