diff --git a/src/Middleware/SpaServices.Extensions/src/ReactDevelopmentServer/ReactDevelopmentServerMiddleware.cs b/src/Middleware/SpaServices.Extensions/src/DevelopmentServer/DevelopmentServerMiddleware.cs similarity index 78% rename from src/Middleware/SpaServices.Extensions/src/ReactDevelopmentServer/ReactDevelopmentServerMiddleware.cs rename to src/Middleware/SpaServices.Extensions/src/DevelopmentServer/DevelopmentServerMiddleware.cs index 78a7b4f03f27..37bc88cf4590 100644 --- a/src/Middleware/SpaServices.Extensions/src/ReactDevelopmentServer/ReactDevelopmentServerMiddleware.cs +++ b/src/Middleware/SpaServices.Extensions/src/DevelopmentServer/DevelopmentServerMiddleware.cs @@ -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)) @@ -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 @@ -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 StartCreateReactAppServerAsync( - string sourcePath, string npmScriptName, ILogger logger) + private static async Task 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 { { "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 }; var npmScriptRunner = new NpmScriptRunner( sourcePath, npmScriptName, null, envVars); @@ -79,18 +81,18 @@ private static async Task 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); } } diff --git a/src/Middleware/SpaServices.Extensions/src/DevelopmentServer/DevelopmentServerMiddlewareExtensions.cs b/src/Middleware/SpaServices.Extensions/src/DevelopmentServer/DevelopmentServerMiddlewareExtensions.cs new file mode 100644 index 000000000000..b7b207d76e19 --- /dev/null +++ b/src/Middleware/SpaServices.Extensions/src/DevelopmentServer/DevelopmentServerMiddlewareExtensions.cs @@ -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 +{ + /// + /// Extension methods for enabling React development server middleware support. + /// + public static class DevelopmentServerMiddlewareExtensions + { + /// + /// 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. + /// + /// The . + /// The name of the script in your package.json file that launches the web server. + /// The text snippet identified during the build to indicate the Development Server has compiled and is ready. + /// The name of the Server used in the Console. + public static void UseDevelopmentServer( + this ISpaBuilder spaBuilder, + string npmScript, + string waitText, + string serverName = "App") + { + + 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); + } + + /// + /// 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. + /// + /// The . + /// The name of the script in your package.json file that launches the create-react-app server. + public static void UseReactDevelopmentServer( + this ISpaBuilder spaBuilder, + string npmScript) + { + UseDevelopmentServer(spaBuilder, npmScript, "Starting the development server", "create-react-app"); + } + + /// + /// 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. + /// + /// The . + /// The name of the script in your package.json file that launches the vue-cli-service server. + public static void UseVueDevelopmentServer( + this ISpaBuilder spaBuilder, + string npmScript) + { + UseDevelopmentServer(spaBuilder, npmScript, "Starting development server...", "vue-cli-service"); + } + } +} diff --git a/src/Middleware/SpaServices.Extensions/src/ReactDevelopmentServer/ReactDevelopmentServerMiddlewareExtensions.cs b/src/Middleware/SpaServices.Extensions/src/ReactDevelopmentServer/ReactDevelopmentServerMiddlewareExtensions.cs index f58a6d1a9dd2..83edae6a322b 100644 --- a/src/Middleware/SpaServices.Extensions/src/ReactDevelopmentServer/ReactDevelopmentServerMiddlewareExtensions.cs +++ b/src/Middleware/SpaServices.Extensions/src/ReactDevelopmentServer/ReactDevelopmentServerMiddlewareExtensions.cs @@ -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 @@ -21,23 +22,12 @@ public static class ReactDevelopmentServerMiddlewareExtensions /// /// The . /// The name of the script in your package.json file that launches the create-react-app server. + [Obsolete("Microsoft.AspNetCore.SpaServices.ReactDevelopmentServer.ReactDevelopmentServerMiddlewareExtensions.UseReactDevelopmentServer is deprecated, please use Microsoft.AspNetCore.SpaServices.DevelopmentServer.DevelopmentServerMiddlewareExtensions.UseReactDevelopmentServer instead.")] 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); } } } diff --git a/src/Middleware/SpaServices.Extensions/src/baseline.netcore.json b/src/Middleware/SpaServices.Extensions/src/baseline.netcore.json index 21d6857e98e7..f0b6f7e34390 100644 --- a/src/Middleware/SpaServices.Extensions/src/baseline.netcore.json +++ b/src/Middleware/SpaServices.Extensions/src/baseline.netcore.json @@ -477,7 +477,7 @@ "GenericParameters": [] }, { - "Name": "Microsoft.AspNetCore.SpaServices.ReactDevelopmentServer.ReactDevelopmentServerMiddlewareExtensions", + "Name": "Microsoft.AspNetCore.SpaServices.DevelopmentServer.DevelopmentServerMiddlewareExtensions", "Visibility": "Public", "Kind": "Class", "Abstract": true, @@ -485,6 +485,33 @@ "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", @@ -503,6 +530,25 @@ "Extension": true, "Visibility": "Public", "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "UseVueDevelopmentServer", + "Parameters": [ + { + "Name": "spaBuilder", + "Type": "Microsoft.AspNetCore.SpaServices.ISpaBuilder" + }, + { + "Name": "npmScript", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] } ], "GenericParameters": [] diff --git a/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/React-CSharp/Startup.cs b/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/React-CSharp/Startup.cs index a9c8ec711fd4..2374c180e29a 100644 --- a/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/React-CSharp/Startup.cs +++ b/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/React-CSharp/Startup.cs @@ -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; diff --git a/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/Startup.cs b/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/Startup.cs index 1ec2eb202763..f07be135ea75 100644 --- a/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/Startup.cs +++ b/src/ProjectTemplates/Web.Spa.ProjectTemplates/content/ReactRedux-CSharp/Startup.cs @@ -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;