| File: ReactDevelopmentServer\ReactDevelopmentServerMiddleware.cs | Web Access |
| Project: src\aspnetcore\src\Middleware\Spa\SpaServices.Extensions\src\Microsoft.AspNetCore.SpaServices.Extensions.csproj (Microsoft.AspNetCore.SpaServices.Extensions) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; using System.Globalization; using System.Text.RegularExpressions; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.NodeServices.Npm; using Microsoft.AspNetCore.NodeServices.Util; using Microsoft.AspNetCore.SpaServices.Extensions.Util; using Microsoft.AspNetCore.SpaServices.Util; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.SpaServices.ReactDevelopmentServer; internal static class ReactDevelopmentServerMiddleware { private const string LogCategoryName = "Microsoft.AspNetCore.SpaServices"; private static readonly 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 scriptName) { var pkgManagerCommand = spaBuilder.Options.PackageManagerCommand; var sourcePath = spaBuilder.Options.SourcePath; var devServerPort = spaBuilder.Options.DevServerPort; if (string.IsNullOrEmpty(sourcePath)) { throw new ArgumentException("Property 'SourcePath' cannot be null or empty", nameof(spaBuilder)); } ArgumentException.ThrowIfNullOrEmpty(scriptName); // Start create-react-app and attach to middleware pipeline var appBuilder = spaBuilder.ApplicationBuilder; var applicationStoppingToken = appBuilder.ApplicationServices.GetRequiredService<IHostApplicationLifetime>().ApplicationStopping; var logger = LoggerFinder.GetOrCreateLogger(appBuilder, LogCategoryName); var diagnosticSource = appBuilder.ApplicationServices.GetRequiredService<DiagnosticSource>(); var portTask = StartCreateReactAppServerAsync(sourcePath, scriptName, pkgManagerCommand, devServerPort, logger, diagnosticSource, applicationStoppingToken); SpaProxyingExtensions.UseProxyToSpaDevelopmentServer(spaBuilder, async () => { // On each request, we create a separate startup task with its own timeout. That way, even if // the first request times out, subsequent requests could still work. var timeout = spaBuilder.Options.StartupTimeout; var port = await portTask.WithTimeout(timeout, "The create-react-app server did not start listening for requests " + $"within the timeout period of {timeout.TotalSeconds} seconds. " + "Check the log output for error information."); // 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 return new UriBuilder("http", "localhost", port).Uri; }); } private static async Task<int> StartCreateReactAppServerAsync( string sourcePath, string scriptName, string pkgManagerCommand, int portNumber, ILogger logger, DiagnosticSource diagnosticSource, CancellationToken applicationStoppingToken) { if (portNumber == default(int)) { portNumber = TcpPortFinder.FindAvailablePort(); } if (logger.IsEnabled(LogLevel.Information)) { logger.LogInformation($"Starting create-react-app server on port {portNumber}..."); } var envVars = new Dictionary<string, string> { { "PORT", portNumber.ToString(CultureInfo.InvariantCulture) }, { "BROWSER", "none" }, // We don't want create-react-app to open its own extra browser window pointing to the internal dev server port }; var scriptRunner = new NodeScriptRunner( sourcePath, scriptName, null, envVars, pkgManagerCommand, diagnosticSource, applicationStoppingToken); scriptRunner.AttachToLogger(logger); using (var stdErrReader = new EventedStreamStringReader(scriptRunner.StdErr)) { try { // Although the React 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 scriptRunner.StdOut.WaitForMatch( new Regex("Starting the development server", RegexOptions.None, RegexMatchTimeout)); } catch (EndOfStreamException ex) { throw new InvalidOperationException( $"The {pkgManagerCommand} script '{scriptName}' exited without indicating that the " + "create-react-app server was listening for requests. The error output was: " + $"{stdErrReader.ReadAsString()}", ex); } } return portNumber; } }