Live reloading (currently enabled only for command-line builds - will

add VS support next)
This commit is contained in:
Steve Sanderson 2018-03-01 10:32:58 +00:00
parent 1fe90e2c0f
commit 2c268a4eca
23 changed files with 539 additions and 20 deletions

View File

@ -90,6 +90,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.VisualStudio.Blaz
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestContentPackage", "test\testapps\TestContentPackage\TestContentPackage.csproj", "{C57382BC-EE93-49D5-BC40-5C98AF8AA048}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LiveReloadTestApp", "test\testapps\LiveReloadTestApp\LiveReloadTestApp.csproj", "{0246AA77-1A27-4A67-874B-6EF6F99E414E}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -281,6 +283,7 @@ Global
{F3E02B21-1127-431A-B832-0E53CB72097B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F3E02B21-1127-431A-B832-0E53CB72097B}.DebugNoVSIX|Any CPU.ActiveCfg = Debug|Any CPU
{F3E02B21-1127-431A-B832-0E53CB72097B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F3E02B21-1127-431A-B832-0E53CB72097B}.Release|Any CPU.Build.0 = Release|Any CPU
{F3E02B21-1127-431A-B832-0E53CB72097B}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU
{FF25111E-5A3E-48A3-96D8-08A2C5A2A91C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FF25111E-5A3E-48A3-96D8-08A2C5A2A91C}.Debug|Any CPU.Build.0 = Debug|Any CPU
@ -312,6 +315,14 @@ Global
{C57382BC-EE93-49D5-BC40-5C98AF8AA048}.Release|Any CPU.Build.0 = Release|Any CPU
{C57382BC-EE93-49D5-BC40-5C98AF8AA048}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU
{C57382BC-EE93-49D5-BC40-5C98AF8AA048}.ReleaseNoVSIX|Any CPU.Build.0 = Release|Any CPU
{0246AA77-1A27-4A67-874B-6EF6F99E414E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0246AA77-1A27-4A67-874B-6EF6F99E414E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0246AA77-1A27-4A67-874B-6EF6F99E414E}.DebugNoVSIX|Any CPU.ActiveCfg = Debug|Any CPU
{0246AA77-1A27-4A67-874B-6EF6F99E414E}.DebugNoVSIX|Any CPU.Build.0 = Debug|Any CPU
{0246AA77-1A27-4A67-874B-6EF6F99E414E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0246AA77-1A27-4A67-874B-6EF6F99E414E}.Release|Any CPU.Build.0 = Release|Any CPU
{0246AA77-1A27-4A67-874B-6EF6F99E414E}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU
{0246AA77-1A27-4A67-874B-6EF6F99E414E}.ReleaseNoVSIX|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -352,6 +363,7 @@ Global
{43E39257-7DC1-46BD-9BD9-2319A1313D07} = {F563ABB6-85FB-4CFC-B0D2-1D5130E8246D}
{9088E4E4-B855-457F-AE9E-D86709A5E1F4} = {F563ABB6-85FB-4CFC-B0D2-1D5130E8246D}
{C57382BC-EE93-49D5-BC40-5C98AF8AA048} = {4AE0D35B-D97A-44D0-8392-C9240377DCCE}
{0246AA77-1A27-4A67-874B-6EF6F99E414E} = {4AE0D35B-D97A-44D0-8392-C9240377DCCE}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {504DA352-6788-4DC0-8705-82167E72A4D3}

View File

@ -1,5 +1,6 @@
import { platform } from './Environment';
import { getAssemblyNameFromUrl } from './Platform/DotNet';
import { enableLiveReloading } from './LiveReloading';
import './Rendering/Renderer';
import './Services/Http';
import './Services/UriHelper';
@ -36,6 +37,13 @@ async function boot() {
// Start up the application
platform.callEntryPoint(entryPointAssemblyName, entryPointMethod, []);
// Enable live reloading only if there's a "reload" attribute on the <script> tag.
// In production, this should not be the case.
const reloadUri = thisScriptElem.getAttribute('reload');
if (reloadUri) {
enableLiveReloading(reloadUri);
}
}
function getRequiredBootScriptAttribute(elem: HTMLScriptElement, attributeName: string): string {

View File

@ -0,0 +1,48 @@
export function enableLiveReloading(endpointUri: string) {
listenForReloadEvent(endpointUri);
}
function listenForReloadEvent(endpointUri: string) {
if (!WebSocket) {
console.log('Browser does not support WebSocket, so live reloading will be disabled.');
return;
}
// First, connect to the endpoint
const websocketUri = toAbsoluteWebSocketUri(endpointUri);
const source = new WebSocket(websocketUri);
let allowConnectionFailedErrorReporting = true;
source.onopen = e => {
allowConnectionFailedErrorReporting = false;
};
source.onerror = e => {
if (allowConnectionFailedErrorReporting) {
allowConnectionFailedErrorReporting = false;
console.error(`The client app was compiled with live reloading enabled, but could not open `
+ ` a WebSocket connection to the server at ${websocketUri}\n`
+ `To fix this inconsistency, either run the server in development mode, or compile the `
+ `client app in Release configuration.`);
}
};
// If we're notified that we should reload, then do so
source.onmessage = e => {
if (e.data === 'reload') {
location.reload();
}
};
}
function toAbsoluteWebSocketUri(uri: string) {
const baseUri = document.baseURI;
if (baseUri) {
const lastSlashPos = baseUri.lastIndexOf('/');
const prefix = baseUri.substr(0, lastSlashPos);
uri = prefix + uri;
}
// Scheme must be ws: or wss:
return uri.replace(/^http/, 'ws');
}

View File

@ -26,6 +26,10 @@ namespace Microsoft.AspNetCore.Blazor.Build.Cli.Commands
"Adds a <link rel=stylesheet> tag with the specified 'href' value",
CommandOptionType.MultipleValue);
var reloadUri = command.Option("--reload-uri",
"If specified, enables live reloading and specifies the URI of the notification endpoint.",
CommandOptionType.SingleValue);
var outputPath = command.Option("--output",
"Path to the output file",
CommandOptionType.SingleValue);
@ -55,6 +59,7 @@ namespace Microsoft.AspNetCore.Blazor.Build.Cli.Commands
jsReferences.Values.ToArray(),
cssReferences.Values.ToArray(),
linkerEnabledFlag.HasValue(),
reloadUri.Value(),
outputPath.Value());
return 0;
}

View File

@ -22,6 +22,7 @@ namespace Microsoft.AspNetCore.Blazor.Build
IEnumerable<string> jsReferences,
IEnumerable<string> cssReferences,
bool linkerEnabled,
string reloadUri,
string outputPath)
{
var template = GetTemplate(path);
@ -31,7 +32,7 @@ namespace Microsoft.AspNetCore.Blazor.Build
}
var assemblyName = Path.GetFileNameWithoutExtension(assemblyPath);
var entryPoint = GetAssemblyEntryPoint(assemblyPath);
var updatedContent = GetIndexHtmlContents(template, assemblyName, entryPoint, assemblyReferences, jsReferences, cssReferences, linkerEnabled);
var updatedContent = GetIndexHtmlContents(template, assemblyName, entryPoint, assemblyReferences, jsReferences, cssReferences, linkerEnabled, reloadUri);
var normalizedOutputPath = Normalize(outputPath);
Console.WriteLine("Writing index to: " + normalizedOutputPath);
File.WriteAllText(normalizedOutputPath, updatedContent);
@ -103,7 +104,8 @@ namespace Microsoft.AspNetCore.Blazor.Build
IEnumerable<string> assemblyReferences,
IEnumerable<string> jsReferences,
IEnumerable<string> cssReferences,
bool linkerEnabled)
bool linkerEnabled,
string reloadUri)
{
var resultBuilder = new StringBuilder();
@ -144,7 +146,8 @@ namespace Microsoft.AspNetCore.Blazor.Build
assemblyEntryPoint,
assemblyReferences,
linkerEnabled,
tag.Attributes);
tag.Attributes,
reloadUri);
// Emit tags to reference any specified JS/CSS files
AppendReferenceTags(
@ -175,7 +178,11 @@ namespace Microsoft.AspNetCore.Blazor.Build
case HtmlTokenType.EndOfFile:
// Finally, emit any remaining text from the original source file
resultBuilder.Append(htmlTemplate, currentRangeStartPos, htmlTemplate.Length - currentRangeStartPos);
var remainingLength = htmlTemplate.Length - currentRangeStartPos;
if (remainingLength > 0)
{
resultBuilder.Append(htmlTemplate, currentRangeStartPos, remainingLength);
}
return resultBuilder.ToString();
}
}
@ -202,7 +209,8 @@ namespace Microsoft.AspNetCore.Blazor.Build
string assemblyEntryPoint,
IEnumerable<string> binFiles,
bool linkerEnabled,
List<KeyValuePair<string, string>> attributes)
List<KeyValuePair<string, string>> attributes,
string reloadUri)
{
var assemblyNameWithExtension = $"{assemblyName}.dll";
@ -224,6 +232,11 @@ namespace Microsoft.AspNetCore.Blazor.Build
attributesDict.Remove("linker-enabled");
}
if (!string.IsNullOrEmpty(reloadUri))
{
attributesDict["reload"] = reloadUri;
}
resultBuilder.Append("<script");
foreach (var attributePair in attributesDict)
{

View File

@ -4,7 +4,16 @@
<PropertyGroup>
<DefaultWebContentItemExcludes>$(DefaultWebContentItemExcludes);wwwroot\**</DefaultWebContentItemExcludes>
<!-- By default, live reloading is on for debug builds -->
<UseBlazorLiveReloading Condition="'$(Configuration)' == 'Debug'">true</UseBlazorLiveReloading>
<BlazorLiveReloadUri>/_reload</BlazorLiveReloadUri>
<!-- We can remove this after updating to newer Razor tooling, where it's enabled by default -->
<UseRazorBuildServer>true</UseRazorBuildServer>
</PropertyGroup>
<ItemGroup>
<!-- In case you're using 'dotnet watch', enable reloading when editing .cshtml files -->
<Watch Include="$(ProjectDir)**\*.cshtml" />
</ItemGroup>
</Project>

View File

@ -7,7 +7,7 @@
<PropertyGroup>
<BlazorBuildExe>dotnet &quot;$(MSBuildThisFileDirectory)../tools/Microsoft.AspNetCore.Blazor.Build.dll&quot;</BlazorBuildExe>
<!-- The Blazor build code can only find your referenced assemblies if they are in the output directory -->
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
</PropertyGroup>
@ -23,6 +23,7 @@
</PropertyGroup>
<WriteLinesToFile File="$(BlazorMetadataFilePath)" Lines="$(MSBuildProjectFullPath)" Overwrite="true" Encoding="Unicode"/>
<WriteLinesToFile File="$(BlazorMetadataFilePath)" Lines="$(OutDir)$(AssemblyName).dll" Overwrite="false" Encoding="Unicode"/>
<WriteLinesToFile File="$(BlazorMetadataFilePath)" Condition="'$(UseBlazorLiveReloading)'=='true'" Lines="reload:$(BlazorLiveReloadUri)" Overwrite="false" Encoding="Unicode"/>
<ItemGroup>
<ContentWithTargetPath Include="$(BlazorMetadataFilePath)" TargetPath="$(BlazorMetadataFileName)" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>

View File

@ -11,8 +11,9 @@
<PropertyGroup Label="Blazor build outputs">
<AdditionalMonoLinkerOptions>-c link -u link -t --verbose </AdditionalMonoLinkerOptions>
<BaseBlazorPackageContentOutputPath>dist/_content/</BaseBlazorPackageContentOutputPath>
<BaseBlazorRuntimeOutputPath>dist/_framework/</BaseBlazorRuntimeOutputPath>
<BaseBlazorDistPath>dist/</BaseBlazorDistPath>
<BaseBlazorPackageContentOutputPath>$(BaseBlazorDistPath)_content/</BaseBlazorPackageContentOutputPath>
<BaseBlazorRuntimeOutputPath>$(BaseBlazorDistPath)_framework/</BaseBlazorRuntimeOutputPath>
<BaseBlazorRuntimeBinOutputPath>$(BaseBlazorRuntimeOutputPath)_bin/</BaseBlazorRuntimeBinOutputPath>
<BaseBlazorRuntimeAsmjsOutputPath>$(BaseBlazorRuntimeOutputPath)asmjs/</BaseBlazorRuntimeAsmjsOutputPath>
<BaseBlazorRuntimeWasmOutputPath>$(BaseBlazorRuntimeOutputPath)wasm/</BaseBlazorRuntimeWasmOutputPath>
@ -21,6 +22,7 @@
<BlazorWebRootName>wwwroot/</BlazorWebRootName>
<BlazorIndexHtmlName>Index.html</BlazorIndexHtmlName>
<BlazorOutputIndexHtmlName>$(BlazorIndexHtmlName.ToLowerInvariant())</BlazorOutputIndexHtmlName>
<BlazorBuildCompletedSignalPath>$(BaseBlazorDistPath)__blazorBuildCompleted</BlazorBuildCompletedSignalPath>
</PropertyGroup>
<ItemGroup>

View File

@ -23,6 +23,9 @@
<ItemGroup>
<FileWrites Include="@(BlazorItemOutput->'%(TargetOutputPath)')" />
</ItemGroup>
<PropertyGroup>
<_BlazorDidCopyFilesToOutputDirectory>true</_BlazorDidCopyFilesToOutputDirectory>
</PropertyGroup>
</Target>
<Target Name="_BlazorTrackResolveReferencesDidRun" AfterTargets="ResolveReferences">
@ -47,6 +50,21 @@
<Message Importance="$(_BlazorStatisticsReportImportance)" Text="%(_BlazorStatisticsOutput.Identity)" />
</Target>
<!--
We only issue the reload notification from here if you're *not* building inside VS.
If you are building inside VS, then it's possible you're building an arbitrary collection
of projects of which this is just one, and it's important to wait until all projects in
the build are completed before reloading, so the notification is instead triggered by the
VS extension that can see when a complete build process is finished.
-->
<Target Name="_BlazorIssueLiveReloadNotification"
AfterTargets="Build"
Condition="'$(UseBlazorLiveReloading)'=='true' AND '$(_BlazorDidCopyFilesToOutputDirectory)'=='true' AND '$(BuildingInsideVisualStudio)'!='true'">
<!-- Touch the signal file to trigger a reload -->
<WriteLinesToFile File="$(ProjectDir)$(OutputPath)$(BlazorBuildCompletedSignalPath)" Lines="_" />
<Delete Files="$(ProjectDir)$(OutputPath)$(BlazorBuildCompletedSignalPath)" />
</Target>
<!-- Preparing blazor files for output:
PrepareBlazorOutputs
_PrepareBlazorOutputConfiguration
@ -65,6 +83,7 @@
_TouchBlazorApplicationAssemblies
_GenerateBlazorIndexHtml
_BlazorCopyFilesToOutputDirectory
_BlazorIssueLiveReloadNotification
The process for doing builds goes as follows:
Produce a hash file with the Hash SDK task and write that hash to a marker file.
@ -571,6 +590,7 @@
<BlazorIndexHtmlInput Include="@(BlazorPackageJsRef->'%(FullPath)')" />
<BlazorIndexHtmlInput Include="@(BlazorPackageCssRef->'%(FullPath)')" />
<BlazorIndexHtmlInput Include="@(_BlazorLinkingOption)" />
<BlazorIndexHtmlInput Include="$(BlazorLiveReloadUri)" Condition="'$(UseBlazorLiveReloading)'=='true'" />
</ItemGroup>
<WriteLinesToFile
@ -597,9 +617,10 @@
</ItemGroup>
<PropertyGroup>
<_LinkerEnabledFlag Condition="'$(_BlazorShouldLinkApplicationAssemblies)' != ''">--linker-enabled</_LinkerEnabledFlag>
<_LiveReloadArg Condition="'$(UseBlazorLiveReloading)' == 'true' AND '$(BlazorLiveReloadUri)' != ''">--reload-uri "$(BlazorLiveReloadUri)"</_LiveReloadArg>
</PropertyGroup>
<Exec Command="$(BlazorBuildExe) build @(IntermediateAssembly) --html-page &quot;$(BlazorIndexHtml)&quot; @(_AppReferences->'--reference &quot;%(Identity)&quot;', ' ') @(_JsReferences->'--js &quot;%(Identity)&quot;', ' ') @(_CssReferences->'--css &quot;%(Identity)&quot;', ' ') $(_LinkerEnabledFlag) --output &quot;$(BlazorIndexHtmlOutputPath)&quot;" />
<Exec Command="$(BlazorBuildExe) build @(IntermediateAssembly) --html-page &quot;$(BlazorIndexHtml)&quot; @(_AppReferences->'--reference &quot;%(Identity)&quot;', ' ') @(_JsReferences->'--js &quot;%(Identity)&quot;', ' ') @(_CssReferences->'--css &quot;%(Identity)&quot;', ' ') $(_LinkerEnabledFlag) $(_LiveReloadArg) --output &quot;$(BlazorIndexHtmlOutputPath)&quot;" />
<ItemGroup Condition="Exists('$(BlazorIndexHtmlOutputPath)')">
<_BlazorIndex Include="$(BlazorIndexHtmlOutputPath)" />

View File

@ -42,13 +42,9 @@ namespace Microsoft.AspNetCore.Builder
// hence all the path manipulation here. We shouldn't be hardcoding 'dist' here either.
var env = (IHostingEnvironment)applicationBuilder.ApplicationServices.GetService(typeof(IHostingEnvironment));
var config = BlazorConfig.Read(options.ClientAssemblyPath);
var clientAppBinDir = Path.GetDirectoryName(config.SourceOutputAssemblyPath);
var clientAppDistDir = Path.Combine(
env.ContentRootPath,
Path.Combine(clientAppBinDir, "dist"));
var distDirStaticFiles = new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(clientAppDistDir),
FileProvider = new PhysicalFileProvider(config.DistPath),
ContentTypeProvider = CreateContentTypeProvider(),
};
@ -68,6 +64,15 @@ namespace Microsoft.AspNetCore.Builder
});
}
// Definitely don't open a listener for live reloading in production, even if the
// client app was compiled with live reloading enabled
if (env.IsDevelopment())
{
// Whether or not live reloading is actually enabled depends on the client config
// For release builds, it won't be (by default)
applicationBuilder.UseBlazorLiveReloading(config);
}
// Finally, use SPA fallback routing (serve default page for anything else,
// excluding /_framework/*)
applicationBuilder.MapWhen(IsNotFrameworkDir, childAppBuilder =>

View File

@ -1,6 +1,7 @@
// 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.Linq;
@ -11,12 +12,19 @@ namespace Microsoft.AspNetCore.Blazor.Server
public string SourceMSBuildPath { get; }
public string SourceOutputAssemblyPath { get; }
public string WebRootPath { get; }
public string ReloadUri { get; }
public string DistPath
=> Path.Combine(Path.GetDirectoryName(SourceOutputAssemblyPath), "dist");
public static BlazorConfig Read(string assemblyPath)
=> new BlazorConfig(assemblyPath);
private BlazorConfig(string assemblyPath)
{
// TODO: Instead of assuming the lines are in a specific order, either JSON-encode
// the whole thing, or at least give the lines key prefixes (e.g., "reload:<someuri>")
// so we're not dependent on order and all lines being present.
var configFilePath = Path.ChangeExtension(assemblyPath, ".blazor.config");
var configLines = File.ReadLines(configFilePath).ToList();
SourceMSBuildPath = configLines[0];
@ -29,6 +37,16 @@ namespace Microsoft.AspNetCore.Blazor.Server
{
WebRootPath = webRootPath;
}
const string reloadMarker = "reload:";
var reloadUri = configLines
.Where(line => line.StartsWith(reloadMarker, StringComparison.Ordinal))
.Select(line => line.Substring(reloadMarker.Length))
.FirstOrDefault();
if (!string.IsNullOrEmpty(reloadUri))
{
ReloadUri = reloadUri;
}
}
}
}

View File

@ -0,0 +1,140 @@
// 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 Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.AspNetCore.Blazor.Server
{
internal class LiveReloadingContext
{
// Keep in sync with $(BlazorBuildCompletedSignalPath) in Blazor.MonoRuntime.props
private const string BlazorBuildCompletedSignalFile = "__blazorBuildCompleted";
// If some external automated process is writing multiple files to wwwroot,
// you probably want to wait until they've all been written before reloading.
// Pausing by 500 milliseconds is a crude effort - we might need a different
// mechanism (e.g., waiting until writes have stopped by 500ms).
private const int WebRootUpdateDelayMilliseconds = 500;
private static byte[] _reloadMessage = Encoding.UTF8.GetBytes("reload");
// If we don't hold references to them, then on Linux they get disposed.
// This static would leak memory if you called UseBlazorLiveReloading continually
// throughout the app lifetime, but the intended usage is just during init.
private static readonly List<FileSystemWatcher> _pinnedWatchers = new List<FileSystemWatcher>();
private readonly object _currentReloadListenerLock = new object();
private CancellationTokenSource _currentReloadListener
= new CancellationTokenSource();
public void Attach(IApplicationBuilder applicationBuilder, BlazorConfig config)
{
CreateFileSystemWatchers(config);
AddWebSocketsEndpoint(applicationBuilder, config.ReloadUri);
}
private void AddWebSocketsEndpoint(IApplicationBuilder applicationBuilder, string url)
{
applicationBuilder.UseWebSockets();
applicationBuilder.Use((context, next) =>
{
if (!string.Equals(context.Request.Path, url, StringComparison.Ordinal))
{
return next();
}
if (!context.WebSockets.IsWebSocketRequest)
{
context.Response.StatusCode = 400;
return context.Response.WriteAsync("This endpoint only accepts WebSockets connections.");
}
return HandleWebSocketRequest(
context.WebSockets.AcceptWebSocketAsync(),
context.RequestAborted);
});
}
private async Task HandleWebSocketRequest(Task<WebSocket> webSocketTask, CancellationToken requestAbortedToken)
{
var webSocket = await webSocketTask;
var reloadToken = _currentReloadListener.Token;
// Wait until either we get a signal to trigger a reload, or the client disconnects
// In either case we're done after that. It's the client's job to reload and start
// a new live reloading context.
try
{
var reloadOrRequestAbortedToken = CancellationTokenSource
.CreateLinkedTokenSource(reloadToken, requestAbortedToken)
.Token;
await Task.Delay(-1, reloadOrRequestAbortedToken);
}
catch (TaskCanceledException)
{
if (reloadToken.IsCancellationRequested)
{
await webSocket.SendAsync(
_reloadMessage,
WebSocketMessageType.Text,
true,
requestAbortedToken);
await webSocket.CloseAsync(
WebSocketCloseStatus.NormalClosure,
"Requested reload",
requestAbortedToken);
}
}
}
private void CreateFileSystemWatchers(BlazorConfig config)
{
// Watch for the "build completed" signal in the dist dir
var distFileWatcher = new FileSystemWatcher(config.DistPath);
distFileWatcher.Deleted += (sender, eventArgs) => {
if (eventArgs.Name.Equals(BlazorBuildCompletedSignalFile, StringComparison.Ordinal))
{
RequestReload(0);
}
};
distFileWatcher.EnableRaisingEvents = true;
_pinnedWatchers.Add(distFileWatcher);
// If there's a WebRootPath, watch for any file modification there
// WebRootPath is only used in dev builds, where we want to serve from wwwroot directly
// without requiring the developer to rebuild after changing the static files there.
// In production there's no need for it.
if (!string.IsNullOrEmpty(config.WebRootPath))
{
var webRootWatcher = new FileSystemWatcher(config.WebRootPath);
webRootWatcher.Deleted += (sender, evtArgs) => RequestReload(WebRootUpdateDelayMilliseconds);
webRootWatcher.Created += (sender, evtArgs) => RequestReload(WebRootUpdateDelayMilliseconds);
webRootWatcher.Changed += (sender, evtArgs) => RequestReload(WebRootUpdateDelayMilliseconds);
webRootWatcher.Renamed += (sender, evtArgs) => RequestReload(WebRootUpdateDelayMilliseconds);
webRootWatcher.EnableRaisingEvents = true;
_pinnedWatchers.Add(webRootWatcher);
}
}
private void RequestReload(int delayMilliseconds)
{
Task.Delay(delayMilliseconds).ContinueWith(_ =>
{
lock (_currentReloadListenerLock)
{
// Lock just to be sure two threads don't assign different new CTSs, of which
// only one would later get cancelled.
_currentReloadListener.Cancel();
_currentReloadListener = new CancellationTokenSource();
}
});
}
}
}

View File

@ -0,0 +1,21 @@
// 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 Microsoft.AspNetCore.Builder;
namespace Microsoft.AspNetCore.Blazor.Server
{
internal static class LiveReloadingExtensions
{
public static void UseBlazorLiveReloading(
this IApplicationBuilder applicationBuilder,
BlazorConfig config)
{
if (!string.IsNullOrEmpty(config.ReloadUri))
{
var context = new LiveReloadingContext();
context.Attach(applicationBuilder, config);
}
}
}
}

View File

@ -7,6 +7,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="2.0.1" />
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="2.0.0" />
<PackageReference Include="Microsoft.AspNetCore.WebSockets" Version="2.0.1" />
<PackageReference Include="Microsoft.Extensions.FileProviders.Physical" Version="2.0.0" />
</ItemGroup>

View File

@ -36,7 +36,8 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
assemblyReferences,
jsReferences,
cssReferences,
linkerEnabled: true);
linkerEnabled: true,
reloadUri: "/my/reload");
// Act & Assert: Start and end is not modified (including formatting)
Assert.StartsWith(htmlTemplatePrefix, instance);
@ -53,6 +54,7 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
Assert.Equal("MyApp.Entrypoint.dll", scriptElem.GetAttribute("main"));
Assert.Equal("MyNamespace.MyType::MyMethod", scriptElem.GetAttribute("entrypoint"));
Assert.Equal("System.Abc.dll,MyApp.ClassLib.dll", scriptElem.GetAttribute("references"));
Assert.Equal("/my/reload", scriptElem.GetAttribute("reload"));
Assert.False(scriptElem.HasAttribute("type"));
Assert.Equal(string.Empty, scriptElem.Attributes["custom1"].Value);
Assert.Equal("value", scriptElem.Attributes["custom2"].Value);
@ -68,6 +70,30 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
linkElems.Select(tag => tag.GetAttribute("href")),
cssReferences);
}
[Fact]
public void OmitsReloadAttributeIfNoReloadUriSpecified()
{
// Arrange
var htmlTemplate = "<script type='blazor-boot'></script>";
var assemblyReferences = new string[] { "System.Abc.dll", "MyApp.ClassLib.dll", };
// Act
var fileContents = IndexHtmlWriter.GetIndexHtmlContents(
htmlTemplate,
"MyApp.Entrypoint",
"MyNamespace.MyType::MyMethod",
assemblyReferences,
/* js references */ new string[] {},
/* js references */ new string[] {},
/* linkerEnabled */ true,
/* reloadUri */ null);
// Assert
var parsedHtml = new HtmlParser().Parse(fileContents);
var scriptElem = parsedHtml.QuerySelector("script");
Assert.False(scriptElem.HasAttribute("reload"));
}
[Fact]
public void SuppliesHtmlTemplateUnchangedIfNoBootScriptPresent()
@ -79,7 +105,7 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
var cssReferences = new string[] { "my/styles.css" };
var content = IndexHtmlWriter.GetIndexHtmlContents(
htmlTemplate, "MyApp.Entrypoint", "MyNamespace.MyType::MyMethod", assemblyReferences, jsReferences, cssReferences, linkerEnabled: true);
htmlTemplate, "MyApp.Entrypoint", "MyNamespace.MyType::MyMethod", assemblyReferences, jsReferences, cssReferences, linkerEnabled: true, reloadUri: "/my/reload");
// Assert
Assert.Equal(htmlTemplate, content);

View File

@ -2,25 +2,36 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Hosting;
using System.Collections.Generic;
using DevHostServerProgram = Microsoft.AspNetCore.Blazor.Cli.Server.Program;
namespace Microsoft.AspNetCore.Blazor.E2ETest.Infrastructure.ServerFixtures
{
public class DevHostServerFixture<TProgram> : WebHostServerFixture
{
public string Environment { get; set; }
public string PathBase { get; set; }
public string ContentRoot { get; private set; }
protected override IWebHost CreateWebHost()
{
var sampleSitePath = FindSampleOrTestSitePath(
ContentRoot = FindSampleOrTestSitePath(
typeof(TProgram).Assembly.GetName().Name);
return DevHostServerProgram.BuildWebHost(new string[]
var args = new List<string>
{
"--urls", "http://127.0.0.1:0",
"--contentroot", sampleSitePath,
"--contentroot", ContentRoot,
"--pathbase", PathBase
});
};
if (!string.IsNullOrEmpty(Environment))
{
args.Add("--environment");
args.Add(Environment);
}
return DevHostServerProgram.BuildWebHost(args.ToArray());
}
}
}

View File

@ -24,6 +24,7 @@
<ProjectReference Include="..\..\samples\StandaloneApp\StandaloneApp.csproj" />
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.Blazor.Cli\Microsoft.AspNetCore.Blazor.Cli.csproj" />
<ProjectReference Include="..\testapps\BasicTestApp\BasicTestApp.csproj" />
<ProjectReference Include="..\testapps\LiveReloadTestApp\LiveReloadTestApp.csproj" />
<ProjectReference Include="..\testapps\TestServer\TestServer.csproj" />
</ItemGroup>

View File

@ -0,0 +1,118 @@
// 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 LiveReloadTestApp;
using Microsoft.AspNetCore.Blazor.E2ETest.Infrastructure;
using Microsoft.AspNetCore.Blazor.E2ETest.Infrastructure.ServerFixtures;
using OpenQA.Selenium;
using OpenQA.Selenium.Support.UI;
using System;
using System.Diagnostics;
using System.IO;
using Xunit;
namespace Microsoft.AspNetCore.Blazor.E2ETest.Tests
{
// We need an entirely separate test app for the live reloading tests, because
// otherwise it might break other tests that were running parallel (e.g., if we
// triggered a reload here while other tests were waiting for something to happen).
public class LiveReloadingTest
: ServerTestBase<DevHostServerFixture<LiveReloadTestApp.Program>>
{
private const string ServerPathBase = "/live/reloading/subdir";
private readonly DevHostServerFixture<Program> _serverFixture;
public LiveReloadingTest(BrowserFixture browserFixture, DevHostServerFixture<Program> serverFixture)
: base(browserFixture, serverFixture)
{
_serverFixture = serverFixture;
serverFixture.Environment = "Development"; // Otherwise the server won't accept live reloading connections
serverFixture.PathBase = ServerPathBase;
Navigate(ServerPathBase);
WaitUntilLoaded();
}
[Fact]
public void ReloadsWhenWebRootFilesAreModified()
{
// Verify we have the expected starting point
var jsFileOutputSelector = By.Id("some-js-file-output");
Assert.Equal("initial value", Browser.FindElement(jsFileOutputSelector).Text);
var jsFilePath = Path.Combine(_serverFixture.ContentRoot, "wwwroot", "someJsFile.js");
var origContents = File.ReadAllText(jsFilePath);
try
{
// Edit the source file on disk
var newContents = origContents.Replace("'initial value'", "'modified value'");
File.WriteAllText(jsFilePath, newContents);
// See that the page reloads and reflects the updated source file
new WebDriverWait(Browser, TimeSpan.FromSeconds(30)).Until(
driver => driver.FindElement(jsFileOutputSelector).Text == "modified value");
WaitUntilLoaded();
}
finally
{
// Restore original state
File.WriteAllText(jsFilePath, origContents);
}
}
[Fact]
public void ReloadsWhenBlazorAppRebuilds()
{
// Verify we have the expected starting point
var appElementSelector = By.TagName("app");
Assert.Equal("Hello, world!", Browser.FindElement(appElementSelector).Text);
var cshtmlFilePath = Path.Combine(_serverFixture.ContentRoot, "Home.cshtml");
var origContents = File.ReadAllText(cshtmlFilePath);
try
{
// Edit the source file on disk
var newContents = origContents.Replace("Hello", "Goodbye");
File.WriteAllText(cshtmlFilePath, newContents);
// Trigger build
var buildConfiguration = DetectBuildConfiguration(_serverFixture.ContentRoot);
var buildProcess = Process.Start(new ProcessStartInfo
{
FileName = "dotnet",
Arguments = $"build --no-restore --no-dependencies -c {buildConfiguration}",
WorkingDirectory = _serverFixture.ContentRoot
});
Assert.True(buildProcess.WaitForExit(60 * 1000));
Assert.Equal(0, buildProcess.ExitCode);
// See that the page reloads and reflects the updated source file
new WebDriverWait(Browser, TimeSpan.FromSeconds(30)).Until(
driver => driver.FindElement(appElementSelector).Text == "Goodbye, world!");
}
finally
{
// Restore original state
File.WriteAllText(cshtmlFilePath, origContents);
}
}
private object DetectBuildConfiguration(string contentRoot)
{
// We want the test to issue the build with the same configuration that
// the project was already built with (otherwise there will be errors because
// of having multiple directories under /bin, plus it means we don't need
// to restore and rebuild all dependencies so it's faster)
var binDirInfo = new DirectoryInfo(Path.Combine(contentRoot, "bin"));
var configurationDirs = binDirInfo.GetDirectories();
Assert.Single(configurationDirs);
return configurationDirs[0].Name;
}
private void WaitUntilLoaded()
{
new WebDriverWait(Browser, TimeSpan.FromSeconds(30)).Until(
driver => driver.FindElement(By.TagName("app")).Text != "Loading...");
}
}
}

View File

@ -0,0 +1 @@
<h1>Hello, world!</h1>

View File

@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<!-- Local alternative to <RunArguments>blazor serve</RunArguments> -->
<RunCommand>dotnet</RunCommand>
<RunArguments>run --project ..\..\..\src\Microsoft.AspNetCore.Blazor.Cli serve --pathbase /live/reloading/subdir</RunArguments>
<!-- For CI, force live reloading on even though it builds in release mode -->
<UseBlazorLiveReloading>true</UseBlazorLiveReloading>
</PropertyGroup>
<!-- Local alternative to <PackageReference Include="Microsoft.AspNetCore.Blazor.Build" /> -->
<Import Project="..\..\..\src\Microsoft.AspNetCore.Blazor.Build\ReferenceFromSource.props" />
<ItemGroup>
<ProjectReference Include="..\..\..\src\Microsoft.AspNetCore.Blazor.Browser\Microsoft.AspNetCore.Blazor.Browser.csproj" />
<ProjectReference Include="..\..\..\src\Microsoft.AspNetCore.Blazor\Microsoft.AspNetCore.Blazor.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,15 @@
// 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 Microsoft.AspNetCore.Blazor.Browser.Rendering;
namespace LiveReloadTestApp
{
public class Program
{
static void Main(string[] args)
{
new BrowserRenderer().AddComponent<Home>("app");
}
}
}

View File

@ -0,0 +1,19 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Live reload test app</title>
<base href="/live/reloading/subdir/" />
</head>
<body>
<app>Loading...</app>
<div id="some-js-file-output"></div>
<script type="blazor-boot"></script>
<script>
// The test about live reloading should be independent of any
// dev server caching headers
document.write("<script src='someJsFile.js?nocache=" + (new Date().valueOf()) + "'></" + "script>");
</script>
</body>
</html>

View File

@ -0,0 +1,3 @@
// We modify this on disk during E2E tests to verify it causes a reload
var valueToWrite = 'initial value';
document.getElementById('some-js-file-output').textContent = valueToWrite;