Skip to content

Abstract ReactDevelopmentServerMiddleware to DevelopmentServerMiddleware for other NPM Web Servers #7452

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,18 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.SpaServices.Extensions.Util;

namespace Microsoft.AspNetCore.SpaServices.ReactDevelopmentServer
namespace Microsoft.AspNetCore.SpaServices.DevelopmentServer
{
internal static class ReactDevelopmentServerMiddleware
internal static class DevelopmentServerMiddleware
{
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

public static void Attach(
ISpaBuilder spaBuilder,
string npmScriptName)
string npmScriptName,
string waitText,
string serverName = "App")
{
var sourcePath = spaBuilder.Options.SourcePath;
if (string.IsNullOrEmpty(sourcePath))
Expand All @@ -38,7 +40,7 @@ public static void Attach(
// 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);
var portTask = StartDevServerAsync(sourcePath, npmScriptName, waitText, serverName, logger);

// Everything we proxy is hardcoded to target http://localhost because:
// - the requests are always from the local machine (we're not accepting remote
Expand All @@ -54,22 +56,22 @@ public static void Attach(
// the first request times out, subsequent requests could still work.
var timeout = spaBuilder.Options.StartupTimeout;
return targetUriTask.WithTimeout(timeout,
$"The create-react-app server did not start listening for requests " +
$"The {serverName} server did not start listening for requests " +
$"within the timeout period of {timeout.Seconds} seconds. " +
$"Check the log output for error information.");
});
}

private static async Task<int> StartCreateReactAppServerAsync(
string sourcePath, string npmScriptName, ILogger logger)
private static async Task<int> StartDevServerAsync(
string sourcePath, string npmScriptName, string waitText, string serverName, ILogger logger)
{
var portNumber = TcpPortFinder.FindAvailablePort();
logger.LogInformation($"Starting create-react-app server on port {portNumber}...");
logger.LogInformation($"Starting {serverName} server on port {portNumber}...");

var envVars = new Dictionary<string, string>
{
{ "PORT", portNumber.ToString() },
{ "BROWSER", "none" }, // We don't want create-react-app to open its own extra browser window pointing to the internal dev server port
{ "BROWSER", "none" }, // We don't want the dev server to open its own extra browser window pointing to the internal dev server port
Copy link
Member

@SteveSandersonMS SteveSandersonMS Feb 25, 2019

Choose a reason for hiding this comment

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

I would have thought this shouldn't be here, as it's specific to React. Could the method take a dictionary of environment variables as a parameter instead?

};
var npmScriptRunner = new NpmScriptRunner(
sourcePath, npmScriptName, null, envVars);
Expand All @@ -79,18 +81,18 @@ private static async Task<int> StartCreateReactAppServerAsync(
{
try
{
// Although the React dev server may eventually tell us the URL it's listening on,
// Although the dev server may eventually tell us the URL it's listening on,
// it doesn't do so until it's finished compiling, and even then only if there were
// no compiler warnings. So instead of waiting for that, consider it ready as soon
// as it starts listening for requests.
await npmScriptRunner.StdOut.WaitForMatch(
new Regex("Starting the development server", RegexOptions.None, RegexMatchTimeout));
new Regex(waitText, RegexOptions.None, RegexMatchTimeout));
}
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: " +
$"{serverName} server was listening for requests. The error output was: " +
$"{stdErrReader.ReadAsString()}", ex);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// 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.DevelopmentServer
{
/// <summary>
/// Extension methods for enabling React development server middleware support.
/// </summary>
public static class DevelopmentServerMiddlewareExtensions
{
/// <summary>
/// Handles requests by passing them through to an instance of a development npm web server.
/// This means you can always serve up-to-date CLI-built resources without having
/// to run the npm web server manually.
///
/// This feature should only be used in development. For production deployments, be
/// sure not to enable the npm web 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 web server.</param>
/// <param name="waitText">The text snippet identified during the build to indicate the Development Server has compiled and is ready.</param>
/// <param name="serverName">The name of the Server used in the Console.</param>
public static void UseDevelopmentServer(
this ISpaBuilder spaBuilder,
string npmScript,
string waitText,
string serverName = "App")
Copy link
Member

@SteveSandersonMS SteveSandersonMS Feb 25, 2019

Choose a reason for hiding this comment

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

To me, these APIs feel extremely over-specialized. What if the "ready" signal isn't some static text you can look for? What if the way to start it isn't running an NPM script?

What I'd expect instead would be to decompose this into lower-level building blocks, e.g.:

  • Some method for starting an NPM script and getting back the Process
  • Some method for transforming the standard output stream from a Process into an async-enumerable-of-string
  • UseReactDevelopmentServer and UseAngularCli methods that call the "start NPM task" method, then awaits until it outputs the expected "I'm running now" string. These can each contain their own instructions to output text like "Starting Angular CLI server...", so no need to make these into parameters on something else.

What do you think - could this be an improvement?

{

if (string.IsNullOrEmpty(waitText))
{
throw new InvalidOperationException($"To use {nameof(UseDevelopmentServer)}, you must supply a non-empty value for the {nameof(waitText)} parameter. This allows us the find when the Development Server has started.");
}

if (spaBuilder == null)
{
throw new ArgumentNullException(nameof(spaBuilder));
}

var spaOptions = spaBuilder.Options;

if (string.IsNullOrEmpty(spaOptions.SourcePath))
{
throw new InvalidOperationException($"To use {nameof(UseDevelopmentServer)}, you must supply a non-empty value for the {nameof(SpaOptions.SourcePath)} property of {nameof(SpaOptions)} when calling {nameof(SpaApplicationBuilderExtensions.UseSpa)}.");
}

DevelopmentServerMiddleware.Attach(spaBuilder, npmScript, waitText, serverName);
}

/// <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)
{
UseDevelopmentServer(spaBuilder, npmScript, "Starting the development server", "create-react-app");
}

/// <summary>
/// Handles requests by passing them through to an instance of the vue-cli-service server.
/// This means you can always serve up-to-date CLI-built resources without having
/// to run the vue-cli-service server manually.
///
/// This feature should only be used in development. For production deployments, be
/// sure not to enable the vue-cli-service 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 vue-cli-service server.</param>
public static void UseVueDevelopmentServer(
Copy link
Member

@SteveSandersonMS SteveSandersonMS Feb 25, 2019

Choose a reason for hiding this comment

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

I appreciate that this adds a feature, but we haven't announced any plan to build in support for Vue-specific things. Previously, we explicitly stepped away from doing that, so we could focus on the two most demanded third-party SPA frameworks (Angular and React).

If we were to change this plan (and it might be that, for the time being, it doesn't change) that would be a decision for @danroth27.

this ISpaBuilder spaBuilder,
string npmScript)
{
UseDevelopmentServer(spaBuilder, npmScript, "Starting development server...", "vue-cli-service");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// 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.SpaServices.DevelopmentServer;
using System;

namespace Microsoft.AspNetCore.SpaServices.ReactDevelopmentServer
Expand All @@ -21,23 +22,12 @@ public static class ReactDevelopmentServerMiddlewareExtensions
/// </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>
[Obsolete("Microsoft.AspNetCore.SpaServices.ReactDevelopmentServer.ReactDevelopmentServerMiddlewareExtensions.UseReactDevelopmentServer is deprecated, please use Microsoft.AspNetCore.SpaServices.DevelopmentServer.DevelopmentServerMiddlewareExtensions.UseReactDevelopmentServer instead.")]
Copy link
Member

Choose a reason for hiding this comment

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

Rather than obsoleting this, could the new logic go here instead of making a new extension method for it?

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);
DevelopmentServerMiddlewareExtensions.UseReactDevelopmentServer(spaBuilder, npmScript);
}
}
}
48 changes: 47 additions & 1 deletion src/Middleware/SpaServices.Extensions/src/baseline.netcore.json
Original file line number Diff line number Diff line change
Expand Up @@ -477,14 +477,41 @@
"GenericParameters": []
},
{
"Name": "Microsoft.AspNetCore.SpaServices.ReactDevelopmentServer.ReactDevelopmentServerMiddlewareExtensions",
"Name": "Microsoft.AspNetCore.SpaServices.DevelopmentServer.DevelopmentServerMiddlewareExtensions",
"Visibility": "Public",
"Kind": "Class",
"Abstract": true,
"Static": true,
"Sealed": true,
"ImplementedInterfaces": [],
"Members": [
{
"Kind": "Method",
"Name": "UseDevelopmentServer",
"Parameters": [
{
"Name": "spaBuilder",
"Type": "Microsoft.AspNetCore.SpaServices.ISpaBuilder"
},
{
"Name": "npmScript",
"Type": "System.String"
},
{
"Name": "waitText",
"Type": "System.String"
},
{
"Name": "serverName",
"Type": "System.String"
}
],
"ReturnType": "System.Void",
"Static": true,
"Extension": true,
"Visibility": "Public",
"GenericParameter": []
},
{
"Kind": "Method",
"Name": "UseReactDevelopmentServer",
Expand All @@ -503,6 +530,25 @@
"Extension": true,
"Visibility": "Public",
"GenericParameter": []
},
{
"Kind": "Method",
"Name": "UseVueDevelopmentServer",
Copy link
Member

@SteveSandersonMS SteveSandersonMS Feb 25, 2019

Choose a reason for hiding this comment

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

As per the comment above, this should only be added after an explicit product decision (which might not happen!)

"Parameters": [
{
"Name": "spaBuilder",
"Type": "Microsoft.AspNetCore.SpaServices.ISpaBuilder"
},
{
"Name": "npmScript",
"Type": "System.String"
}
],
"ReturnType": "System.Void",
"Static": true,
"Extension": true,
"Visibility": "Public",
"GenericParameter": []
}
],
"GenericParameters": []
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
using Microsoft.AspNetCore.HttpsPolicy;
#endif
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SpaServices.ReactDevelopmentServer;
using Microsoft.AspNetCore.SpaServices.DevelopmentServer;
#if (IndividualLocalAuth)
using Microsoft.EntityFrameworkCore;
using Company.WebApplication1.Data;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
using Microsoft.AspNetCore.HttpsPolicy;
#endif
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SpaServices.ReactDevelopmentServer;
using Microsoft.AspNetCore.SpaServices.DevelopmentServer;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
Expand Down