mirror of
https://github.com/aspnet/JavaScriptServices.git
synced 2025-12-22 17:47:53 +00:00
Add UseReactDevelopmentServer() middleware. Factor out common code.
This commit is contained in:
@@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Builder;
|
|||||||
using Microsoft.AspNetCore.NodeServices.Npm;
|
using Microsoft.AspNetCore.NodeServices.Npm;
|
||||||
using Microsoft.AspNetCore.NodeServices.Util;
|
using Microsoft.AspNetCore.NodeServices.Util;
|
||||||
using Microsoft.AspNetCore.SpaServices.Prerendering;
|
using Microsoft.AspNetCore.SpaServices.Prerendering;
|
||||||
|
using Microsoft.AspNetCore.SpaServices.Util;
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
@@ -46,11 +47,14 @@ namespace Microsoft.AspNetCore.SpaServices.AngularCli
|
|||||||
throw new InvalidOperationException($"To use {nameof(AngularCliBuilder)}, you must supply a non-empty value for the {nameof(SpaOptions.SourcePath)} property of {nameof(SpaOptions)} when calling {nameof(SpaApplicationBuilderExtensions.UseSpa)}.");
|
throw new InvalidOperationException($"To use {nameof(AngularCliBuilder)}, you must supply a non-empty value for the {nameof(SpaOptions.SourcePath)} property of {nameof(SpaOptions)} when calling {nameof(SpaApplicationBuilderExtensions.UseSpa)}.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var logger = AngularCliMiddleware.GetOrCreateLogger(spaBuilder.ApplicationBuilder);
|
var logger = LoggerFinder.GetOrCreateLogger(
|
||||||
|
spaBuilder.ApplicationBuilder,
|
||||||
|
nameof(AngularCliBuilder));
|
||||||
var npmScriptRunner = new NpmScriptRunner(
|
var npmScriptRunner = new NpmScriptRunner(
|
||||||
sourcePath,
|
sourcePath,
|
||||||
_npmScriptName,
|
_npmScriptName,
|
||||||
"--watch");
|
"--watch",
|
||||||
|
null);
|
||||||
npmScriptRunner.AttachToLogger(logger);
|
npmScriptRunner.AttachToLogger(logger);
|
||||||
|
|
||||||
using (var stdErrReader = new EventedStreamStringReader(npmScriptRunner.StdErr))
|
using (var stdErrReader = new EventedStreamStringReader(npmScriptRunner.StdErr))
|
||||||
|
|||||||
@@ -2,17 +2,14 @@
|
|||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// 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.Builder;
|
||||||
using System;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Microsoft.AspNetCore.NodeServices.Npm;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.AspNetCore.NodeServices.Npm;
|
||||||
using Microsoft.Extensions.Logging.Console;
|
|
||||||
using System.Net.Sockets;
|
|
||||||
using System.Net;
|
|
||||||
using System.IO;
|
|
||||||
using Microsoft.AspNetCore.NodeServices.Util;
|
using Microsoft.AspNetCore.NodeServices.Util;
|
||||||
|
using Microsoft.AspNetCore.SpaServices.Util;
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.SpaServices.AngularCli
|
namespace Microsoft.AspNetCore.SpaServices.AngularCli
|
||||||
{
|
{
|
||||||
@@ -39,7 +36,7 @@ namespace Microsoft.AspNetCore.SpaServices.AngularCli
|
|||||||
|
|
||||||
// Start Angular CLI and attach to middleware pipeline
|
// Start Angular CLI and attach to middleware pipeline
|
||||||
var appBuilder = spaBuilder.ApplicationBuilder;
|
var appBuilder = spaBuilder.ApplicationBuilder;
|
||||||
var logger = GetOrCreateLogger(appBuilder);
|
var logger = LoggerFinder.GetOrCreateLogger(appBuilder, LogCategoryName);
|
||||||
var angularCliServerInfoTask = StartAngularCliServerAsync(sourcePath, npmScriptName, logger);
|
var angularCliServerInfoTask = StartAngularCliServerAsync(sourcePath, npmScriptName, logger);
|
||||||
|
|
||||||
// Everything we proxy is hardcoded to target http://localhost because:
|
// Everything we proxy is hardcoded to target http://localhost because:
|
||||||
@@ -53,24 +50,14 @@ namespace Microsoft.AspNetCore.SpaServices.AngularCli
|
|||||||
SpaProxyingExtensions.UseProxyToSpaDevelopmentServer(spaBuilder, targetUriTask);
|
SpaProxyingExtensions.UseProxyToSpaDevelopmentServer(spaBuilder, targetUriTask);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static ILogger GetOrCreateLogger(IApplicationBuilder appBuilder)
|
|
||||||
{
|
|
||||||
// If the DI system gives us a logger, use it. Otherwise, set up a default one.
|
|
||||||
var loggerFactory = appBuilder.ApplicationServices.GetService<ILoggerFactory>();
|
|
||||||
var logger = loggerFactory != null
|
|
||||||
? loggerFactory.CreateLogger(LogCategoryName)
|
|
||||||
: new ConsoleLogger(LogCategoryName, null, false);
|
|
||||||
return logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task<AngularCliServerInfo> StartAngularCliServerAsync(
|
private static async Task<AngularCliServerInfo> StartAngularCliServerAsync(
|
||||||
string sourcePath, string npmScriptName, ILogger logger)
|
string sourcePath, string npmScriptName, ILogger logger)
|
||||||
{
|
{
|
||||||
var portNumber = FindAvailablePort();
|
var portNumber = TcpPortFinder.FindAvailablePort();
|
||||||
logger.LogInformation($"Starting @angular/cli on port {portNumber}...");
|
logger.LogInformation($"Starting @angular/cli on port {portNumber}...");
|
||||||
|
|
||||||
var npmScriptRunner = new NpmScriptRunner(
|
var npmScriptRunner = new NpmScriptRunner(
|
||||||
sourcePath, npmScriptName, $"--port {portNumber}");
|
sourcePath, npmScriptName, $"--port {portNumber}", null);
|
||||||
npmScriptRunner.AttachToLogger(logger);
|
npmScriptRunner.AttachToLogger(logger);
|
||||||
|
|
||||||
Match openBrowserLine;
|
Match openBrowserLine;
|
||||||
@@ -109,20 +96,6 @@ namespace Microsoft.AspNetCore.SpaServices.AngularCli
|
|||||||
return serverInfo;
|
return serverInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int FindAvailablePort()
|
|
||||||
{
|
|
||||||
var listener = new TcpListener(IPAddress.Loopback, 0);
|
|
||||||
listener.Start();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return ((IPEndPoint)listener.LocalEndpoint).Port;
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
listener.Stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class AngularCliServerInfo
|
class AngularCliServerInfo
|
||||||
{
|
{
|
||||||
public int Port { get; set; }
|
public int Port { get; set; }
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ using System;
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
// This is under the NodeServices namespace because post 2.1 it will be moved to that package
|
// This is under the NodeServices namespace because post 2.1 it will be moved to that package
|
||||||
namespace Microsoft.AspNetCore.NodeServices.Npm
|
namespace Microsoft.AspNetCore.NodeServices.Npm
|
||||||
@@ -22,7 +23,7 @@ namespace Microsoft.AspNetCore.NodeServices.Npm
|
|||||||
|
|
||||||
private static Regex AnsiColorRegex = new Regex("\x001b\\[[0-9;]*m", RegexOptions.None, TimeSpan.FromSeconds(1));
|
private static Regex AnsiColorRegex = new Regex("\x001b\\[[0-9;]*m", RegexOptions.None, TimeSpan.FromSeconds(1));
|
||||||
|
|
||||||
public NpmScriptRunner(string workingDirectory, string scriptName, string arguments)
|
public NpmScriptRunner(string workingDirectory, string scriptName, string arguments, IDictionary<string, string> envVars)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(workingDirectory))
|
if (string.IsNullOrEmpty(workingDirectory))
|
||||||
{
|
{
|
||||||
@@ -45,7 +46,7 @@ namespace Microsoft.AspNetCore.NodeServices.Npm
|
|||||||
completeArguments = $"/c npm {completeArguments}";
|
completeArguments = $"/c npm {completeArguments}";
|
||||||
}
|
}
|
||||||
|
|
||||||
var process = LaunchNodeProcess(new ProcessStartInfo(npmExe)
|
var processStartInfo = new ProcessStartInfo(npmExe)
|
||||||
{
|
{
|
||||||
Arguments = completeArguments,
|
Arguments = completeArguments,
|
||||||
UseShellExecute = false,
|
UseShellExecute = false,
|
||||||
@@ -53,8 +54,17 @@ namespace Microsoft.AspNetCore.NodeServices.Npm
|
|||||||
RedirectStandardOutput = true,
|
RedirectStandardOutput = true,
|
||||||
RedirectStandardError = true,
|
RedirectStandardError = true,
|
||||||
WorkingDirectory = workingDirectory
|
WorkingDirectory = workingDirectory
|
||||||
});
|
};
|
||||||
|
|
||||||
|
if (envVars != null)
|
||||||
|
{
|
||||||
|
foreach (var keyValuePair in envVars)
|
||||||
|
{
|
||||||
|
processStartInfo.Environment[keyValuePair.Key] = keyValuePair.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var process = LaunchNodeProcess(processStartInfo);
|
||||||
StdOut = new EventedStreamReader(process.StandardOutput);
|
StdOut = new EventedStreamReader(process.StandardOutput);
|
||||||
StdErr = new EventedStreamReader(process.StandardError);
|
StdErr = new EventedStreamReader(process.StandardError);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
// 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.Extensions.Logging;
|
||||||
|
using Microsoft.AspNetCore.NodeServices.Npm;
|
||||||
|
using Microsoft.AspNetCore.NodeServices.Util;
|
||||||
|
using Microsoft.AspNetCore.SpaServices.Util;
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.SpaServices.ReactDevelopmentServer
|
||||||
|
{
|
||||||
|
internal static class ReactDevelopmentServerMiddleware
|
||||||
|
{
|
||||||
|
private const string LogCategoryName = "Microsoft.AspNetCore.SpaServices";
|
||||||
|
private static TimeSpan RegexMatchTimeout = TimeSpan.FromSeconds(5); // This is a development-time only feature, so a very long timeout is fine
|
||||||
|
private static TimeSpan StartupTimeout = TimeSpan.FromSeconds(50); // Note that the HTTP request itself by default times out after 60s, so you only get useful error information if this is shorter
|
||||||
|
|
||||||
|
public static void Attach(
|
||||||
|
ISpaBuilder spaBuilder,
|
||||||
|
string npmScriptName)
|
||||||
|
{
|
||||||
|
var sourcePath = spaBuilder.Options.SourcePath;
|
||||||
|
if (string.IsNullOrEmpty(sourcePath))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Cannot be null or empty", nameof(sourcePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(npmScriptName))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Cannot be null or empty", nameof(npmScriptName));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start create-react-app and attach to middleware pipeline
|
||||||
|
var appBuilder = spaBuilder.ApplicationBuilder;
|
||||||
|
var logger = LoggerFinder.GetOrCreateLogger(appBuilder, LogCategoryName);
|
||||||
|
var portTask = StartCreateReactAppServerAsync(sourcePath, npmScriptName, logger);
|
||||||
|
|
||||||
|
// Everything we proxy is hardcoded to target http://localhost because:
|
||||||
|
// - the requests are always from the local machine (we're not accepting remote
|
||||||
|
// requests that go directly to the create-react-app server)
|
||||||
|
// - given that, there's no reason to use https, and we couldn't even if we
|
||||||
|
// wanted to, because in general the create-react-app server has no certificate
|
||||||
|
var targetUriTask = portTask.ContinueWith(
|
||||||
|
task => new UriBuilder("http", "localhost", task.Result).Uri);
|
||||||
|
|
||||||
|
SpaProxyingExtensions.UseProxyToSpaDevelopmentServer(spaBuilder, targetUriTask);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<int> StartCreateReactAppServerAsync(
|
||||||
|
string sourcePath, string npmScriptName, ILogger logger)
|
||||||
|
{
|
||||||
|
var portNumber = TcpPortFinder.FindAvailablePort();
|
||||||
|
logger.LogInformation($"Starting create-react-app server on port {portNumber}...");
|
||||||
|
|
||||||
|
var envVars = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ "PORT", portNumber.ToString() }
|
||||||
|
};
|
||||||
|
var npmScriptRunner = new NpmScriptRunner(
|
||||||
|
sourcePath, npmScriptName, null, envVars);
|
||||||
|
npmScriptRunner.AttachToLogger(logger);
|
||||||
|
|
||||||
|
Match openBrowserLine;
|
||||||
|
using (var stdErrReader = new EventedStreamStringReader(npmScriptRunner.StdErr))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
openBrowserLine = await npmScriptRunner.StdOut.WaitForMatch(
|
||||||
|
new Regex("Local:\\s*(http\\S+)", RegexOptions.None, RegexMatchTimeout),
|
||||||
|
StartupTimeout);
|
||||||
|
}
|
||||||
|
catch (EndOfStreamException ex)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"The NPM script '{npmScriptName}' exited without indicating that the " +
|
||||||
|
$"create-react-app server was listening for requests. The error output was: " +
|
||||||
|
$"{stdErrReader.ReadAsString()}", ex);
|
||||||
|
}
|
||||||
|
catch (TaskCanceledException ex)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"The create-react-app server did not start listening for requests " +
|
||||||
|
$"within the timeout period of {StartupTimeout.Seconds} seconds. " +
|
||||||
|
$"Check the log output for error information.", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var uri = new Uri(openBrowserLine.Groups[1].Value);
|
||||||
|
return uri.Port;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
// 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 System;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.SpaServices.ReactDevelopmentServer
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Extension methods for enabling React development server middleware support.
|
||||||
|
/// </summary>
|
||||||
|
public static class ReactDevelopmentServerMiddlewareExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Handles requests by passing them through to an instance of the create-react-app server.
|
||||||
|
/// This means you can always serve up-to-date CLI-built resources without having
|
||||||
|
/// to run the create-react-app server manually.
|
||||||
|
///
|
||||||
|
/// This feature should only be used in development. For production deployments, be
|
||||||
|
/// sure not to enable the create-react-app server.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="spaBuilder">The <see cref="ISpaBuilder"/>.</param>
|
||||||
|
/// <param name="npmScript">The name of the script in your package.json file that launches the create-react-app server.</param>
|
||||||
|
public static void UseReactDevelopmentServer(
|
||||||
|
this ISpaBuilder spaBuilder,
|
||||||
|
string npmScript)
|
||||||
|
{
|
||||||
|
if (spaBuilder == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(spaBuilder));
|
||||||
|
}
|
||||||
|
|
||||||
|
var spaOptions = spaBuilder.Options;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(spaOptions.SourcePath))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"To use {nameof(UseReactDevelopmentServer)}, you must supply a non-empty value for the {nameof(SpaOptions.SourcePath)} property of {nameof(SpaOptions)} when calling {nameof(SpaApplicationBuilderExtensions.UseSpa)}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
ReactDevelopmentServerMiddleware.Attach(spaBuilder, npmScript);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
// 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.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Logging.Console;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.SpaServices.Util
|
||||||
|
{
|
||||||
|
internal static class LoggerFinder
|
||||||
|
{
|
||||||
|
public static ILogger GetOrCreateLogger(
|
||||||
|
IApplicationBuilder appBuilder,
|
||||||
|
string logCategoryName)
|
||||||
|
{
|
||||||
|
// If the DI system gives us a logger, use it. Otherwise, set up a default one.
|
||||||
|
var loggerFactory = appBuilder.ApplicationServices.GetService<ILoggerFactory>();
|
||||||
|
var logger = loggerFactory != null
|
||||||
|
? loggerFactory.CreateLogger(logCategoryName)
|
||||||
|
: new ConsoleLogger(logCategoryName, null, false);
|
||||||
|
return logger;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
// 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.Net;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.SpaServices.Util
|
||||||
|
{
|
||||||
|
internal static class TcpPortFinder
|
||||||
|
{
|
||||||
|
public static int FindAvailablePort()
|
||||||
|
{
|
||||||
|
var listener = new TcpListener(IPAddress.Loopback, 0);
|
||||||
|
listener.Start();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return ((IPEndPoint)listener.LocalEndpoint).Port;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
listener.Stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user