Live reloading (currently enabled only for command-line builds - will
add VS support next)
This commit is contained in:
parent
1fe90e2c0f
commit
2c268a4eca
12
Blazor.sln
12
Blazor.sln
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
<PropertyGroup>
|
||||
<BlazorBuildExe>dotnet "$(MSBuildThisFileDirectory)../tools/Microsoft.AspNetCore.Blazor.Build.dll"</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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 "$(BlazorIndexHtml)" @(_AppReferences->'--reference "%(Identity)"', ' ') @(_JsReferences->'--js "%(Identity)"', ' ') @(_CssReferences->'--css "%(Identity)"', ' ') $(_LinkerEnabledFlag) --output "$(BlazorIndexHtmlOutputPath)"" />
|
||||
<Exec Command="$(BlazorBuildExe) build @(IntermediateAssembly) --html-page "$(BlazorIndexHtml)" @(_AppReferences->'--reference "%(Identity)"', ' ') @(_JsReferences->'--js "%(Identity)"', ' ') @(_CssReferences->'--css "%(Identity)"', ' ') $(_LinkerEnabledFlag) $(_LiveReloadArg) --output "$(BlazorIndexHtmlOutputPath)"" />
|
||||
|
||||
<ItemGroup Condition="Exists('$(BlazorIndexHtmlOutputPath)')">
|
||||
<_BlazorIndex Include="$(BlazorIndexHtmlOutputPath)" />
|
||||
|
|
|
|||
|
|
@ -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 =>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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...");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
<h1>Hello, world!</h1>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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;
|
||||
Loading…
Reference in New Issue