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)
        {
            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 Vue CLI and attach to middleware pipeline
            var appBuilder           = spaBuilder.ApplicationBuilder;
            var logger               = LoggerFinder.GetOrCreateLogger(appBuilder, LogCategoryName);
            var vueCliServerInfoTask = StartVueCliServerAsync(sourcePath, npmScriptName, logger);

            var targetUriTask = vueCliServerInfoTask.ContinueWith(
                task => new UriBuilder(task.Result.Scheme, task.Result.Host, task.Result.Port).Uri);

            SpaProxyingExtensions.UseProxyToSpaDevelopmentServer(spaBuilder, () =>
            {
                // 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;
                return(targetUriTask.WithTimeout(timeout,
                                                 $"The Vue CLI process did not start listening for requests " +
                                                 $"within the timeout period of {timeout.Seconds} seconds. " +
                                                 $"Check the log output for error information."));
            });
        }
        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);
        }
Exemple #3
0
        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));
            }

            var appBuilder = spaBuilder.ApplicationBuilder;
            var logger     = LoggerFinder.GetOrCreateLogger(appBuilder, LogCategoryName);
            var portTask   = StartVueDevServerAsync(sourcePath, npmScriptName, logger);

            var targetUriTask = portTask.ContinueWith(
                task => new UriBuilder("http", "localhost", task.Result).Uri);

            SpaProxyingExtensions.UseProxyToSpaDevelopmentServer(spaBuilder, () =>
            {
                var timeout = spaBuilder.Options.StartupTimeout;
                return(targetUriTask.WithTimeout(timeout,
                                                 $"The vue development server did not start listening for requests " +
                                                 $"within the timeout period of {timeout.Seconds} seconds. " +
                                                 $"Check the log output for error information."));
            });
        }
Exemple #4
0
        /// <inheritdoc />
        public async Task Build(ISpaBuilder spaBuilder)
        {
            var pkgManagerCommand = spaBuilder.Options.PackageManagerCommand;
            var sourcePath        = spaBuilder.Options.SourcePath;

            if (string.IsNullOrEmpty(sourcePath))
            {
                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 appBuilder = spaBuilder.ApplicationBuilder;
            var applicationStoppingToken = appBuilder.ApplicationServices.GetRequiredService <IHostApplicationLifetime>().ApplicationStopping;
            var logger = LoggerFinder.GetOrCreateLogger(
                appBuilder,
                nameof(AngularCliBuilder));
            var diagnosticSource = appBuilder.ApplicationServices.GetRequiredService <DiagnosticSource>();
            var scriptRunner     = new NodeScriptRunner(
                sourcePath,
                _scriptName,
                "--watch",
                null,
                pkgManagerCommand,
                diagnosticSource,
                applicationStoppingToken);

            scriptRunner.AttachToLogger(logger);

            using (var stdOutReader = new EventedStreamStringReader(scriptRunner.StdOut))
                using (var stdErrReader = new EventedStreamStringReader(scriptRunner.StdErr))
                {
                    try
                    {
                        await scriptRunner.StdOut.WaitForMatch(
                            new Regex("Date", RegexOptions.None, RegexMatchTimeout));
                    }
                    catch (EndOfStreamException ex)
                    {
                        throw new InvalidOperationException(
                                  $"The {pkgManagerCommand} script '{_scriptName}' exited without indicating success.\n" +
                                  $"Output was: {stdOutReader.ReadAsString()}\n" +
                                  $"Error output was: {stdErrReader.ReadAsString()}", ex);
                    }
                    catch (OperationCanceledException ex)
                    {
                        throw new InvalidOperationException(
                                  $"The {pkgManagerCommand} script '{_scriptName}' timed out without indicating success. " +
                                  $"Output was: {stdOutReader.ReadAsString()}\n" +
                                  $"Error output was: {stdErrReader.ReadAsString()}", ex);
                    }
                }
        }
        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(
            IApplicationBuilder appBuilder,
            string sourcePath,
            string npmScriptName)
        {
            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));
            }

            var logger = LoggerFinder.GetOrCreateLogger(appBuilder, LogCategoryName);

            // Start Vue devevelopment server
            var portTask      = StartVueDevServerAsync(appBuilder, sourcePath, npmScriptName, logger);
            var targetUriTask = portTask.ContinueWith(
                task => new UriBuilder("http", "localhost", task.Result).Uri);

            // Add middleware to the pipeline that waits for the Vue development server to start
            appBuilder.Use(async(context, next) =>
            {
                // 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 = TimeSpan.FromSeconds(90); // this is a dev only middleware, long timeout is probably fine
                await targetUriTask.WithTimeout(timeout,
                                                $"The vue development server did not start listening for requests " +
                                                $"within the timeout period of {timeout.Seconds} seconds. " +
                                                $"Check the log output for error information.");

                await next();
            });

            // Redirect all requests for root towards the Vue development server,
            // using the resolved targetUriTask
            appBuilder.Use(async(context, next) =>
            {
                if (context.Request.Path == "/")
                {
                    var devServerUri = await targetUriTask;
                    context.Response.Redirect(devServerUri.ToString());
                }
                else
                {
                    await next();
                }
            });
        }
Exemple #6
0
        static void Main(string[] args)
        {
            var lf     = new LoggerFinder();
            var logger = lf.FindLoggerPort("COM3");

            var ld = logger?.GetDetailsFromDevice();

            Console.WriteLine($"Serialnumber : {ld?.SerialNumber}");
            Console.WriteLine($"Description  : {ld?.Description}");
            Console.WriteLine($"Info         : {ld?.Info}");

            logger?.Dispose();

            Console.ReadLine();
        }
        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 npmExe,
            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));
            }

            if (string.IsNullOrEmpty(npmExe))
            {
                throw new ArgumentException("Cannot be null or empty", nameof(npmExe));
            }

            // Start create-vue-app and attach to middleware pipeline
            var appBuilder = spaBuilder.ApplicationBuilder;
            var logger     = LoggerFinder.GetOrCreateLogger(appBuilder, LogCategoryName);
            var portTask   = StartCreateVueAppServerAsync(sourcePath, npmExe, 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-Vue-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-Vue-app server has no certificate
            var targetUriTask = portTask.ContinueWith(
                task => new UriBuilder("http", "localhost", task.Result).Uri);

            spaBuilder.UseProxyToSpaDevelopmentServer(() =>
            {
                // 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;
                return(targetUriTask.WithTimeout(timeout,
                                                 $"The create-Vue-app server did not start listening for requests " +
                                                 $"within the timeout period of {timeout.Seconds} seconds. " +
                                                 $"Check the log output for error information."));
            });
        }
        /// <inheritdoc />
        public async Task Build(ISpaBuilder spaBuilder)
        {
            var sourcePath = spaBuilder.Options.SourcePath;

            if (string.IsNullOrEmpty(sourcePath))
            {
                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 = LoggerFinder.GetOrCreateLogger(
                spaBuilder.ApplicationBuilder,
                nameof(AngularCliBuilder));
            var npmScriptRunner = new NpmScriptRunner(
                sourcePath,
                _npmScriptName,
                "--watch",
                null);

            npmScriptRunner.AttachToLogger(logger);

            using (var stdOutReader = new EventedStreamStringReader(npmScriptRunner.StdOut))
                using (var stdErrReader = new EventedStreamStringReader(npmScriptRunner.StdErr))
                {
                    try
                    {
                        await npmScriptRunner.StdOut.WaitForMatch(
                            new Regex("Date", RegexOptions.None, RegexMatchTimeout),
                            BuildTimeout);
                    }
                    catch (EndOfStreamException ex)
                    {
                        throw new InvalidOperationException(
                                  $"The NPM script '{_npmScriptName}' exited without indicating success.\n" +
                                  $"Output was: {stdOutReader.ReadAsString()}\n" +
                                  $"Error output was: {stdErrReader.ReadAsString()}", ex);
                    }
                    catch (OperationCanceledException ex)
                    {
                        throw new InvalidOperationException(
                                  $"The NPM script '{_npmScriptName}' timed out without indicating success. " +
                                  $"Output was: {stdOutReader.ReadAsString()}\n" +
                                  $"Error output was: {stdErrReader.ReadAsString()}", ex);
                    }
                }
        }
Exemple #9
0
        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 scriptName)
        {
            var pkgManagerCommand = spaBuilder.Options.PackageManagerCommand;
            var sourcePath        = spaBuilder.Options.SourcePath;
            var devServerPort     = spaBuilder.Options.DevServerPort;

            if (string.IsNullOrEmpty(sourcePath))
            {
                throw new ArgumentException("Cannot be null or empty", nameof(sourcePath));
            }

            if (string.IsNullOrEmpty(scriptName))
            {
                throw new ArgumentException("Cannot be null or empty", nameof(scriptName));
            }

            // Start Angular CLI 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 angularCliServerInfoTask = StartAngularCliServerAsync(sourcePath, scriptName, pkgManagerCommand, devServerPort, logger, diagnosticSource, applicationStoppingToken);

            // 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 Angular CLI middleware server)
            // - given that, there's no reason to use https, and we couldn't even if we
            //   wanted to, because in general the Angular CLI server has no certificate
            var targetUriTask = angularCliServerInfoTask.ContinueWith(
                task => new UriBuilder("http", "localhost", task.Result.Port).Uri);

            SpaProxyingExtensions.UseProxyToSpaDevelopmentServer(spaBuilder, () =>
            {
                // 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;
                return(targetUriTask.WithTimeout(timeout,
                                                 $"The Angular CLI process did not start listening for requests " +
                                                 $"within the timeout period of {timeout.TotalSeconds} seconds. " +
                                                 $"Check the log output for error information."));
            });
        }
        // /// <summary>
        // /// Configures the application to serve static files for a Single Page Application (SPA).
        // /// The files will be located using the registered <see cref="ISpaStaticFileProvider"/> service.
        // /// </summary>
        // /// <param name="applicationBuilder">The <see cref="IApplicationBuilder"/>.</param>
        // public static void UseSpaStaticFiles(this IApplicationBuilder applicationBuilder, string clientAppDistPath)
        // {
        //     UseMulitSpaStaticFiles(applicationBuilder, clientAppDistPath, new StaticFileOptions());
        // }

        // /// <summary>
        // /// Configures the application to serve static files for a Single Page Application (SPA).
        // /// The files will be located using the registered <see cref="ISpaStaticFileProvider"/> service.
        // /// </summary>
        // /// <param name="applicationBuilder">The <see cref="IApplicationBuilder"/>.</param>
        // /// <param name="options">Specifies options for serving the static files.</param>
        // public static void UseMultipleSpaStaticFiles(this IApplicationBuilder applicationBuilder, string publicPath, StaticFileOptions options)
        // {
        //     if (applicationBuilder == null)
        //     {
        //         throw new ArgumentNullException(nameof(applicationBuilder));
        //     }
        //
        //     if (options == null)
        //     {
        //         throw new ArgumentNullException(nameof(options));
        //     }
        //
        //     UseSpaStaticFilesInternal(applicationBuilder, publicPath, staticFileOptions: options, allowFallbackOnServingWebRootFiles: false);
        // }

        internal static void UseSpaStaticFilesInternal(
            this IApplicationBuilder app,
            string publicPath,
            string clientAppDistPath,
            StaticFileOptions staticFileOptions,
            bool allowFallbackOnServingWebRootFiles)
        {
            var logger = LoggerFinder.GetOrCreateLogger(app, "OpenBlog.SpaStaticFilesExtensions");

            if (staticFileOptions == null)
            {
                logger.LogWarning($"{nameof(staticFileOptions)} argument is null");
                throw new ArgumentNullException(nameof(staticFileOptions));
            }

            // If the file provider was explicitly supplied, that takes precedence over any other
            // configured file provider. This is most useful if the application hosts multiple SPAs
            // (via multiple calls to UseSpa()), so each needs to serve its own separate static files
            // instead of using AddSpaStaticFiles/UseSpaStaticFiles.
            // But if no file provider was specified, try to get one from the DI config.
            if (staticFileOptions.FileProvider == null)
            {
                var shouldServeStaticFiles = ShouldServeStaticFiles(
                    app, publicPath, clientAppDistPath,
                    allowFallbackOnServingWebRootFiles,
                    out var fileProviderOrDefault);
                if (shouldServeStaticFiles)
                {
                    logger.LogInformation($"use static files serve");
                    staticFileOptions.FileProvider = fileProviderOrDefault;
                }
                else
                {
                    logger.LogWarning($"don't serve static files");
                    // The registered ISpaStaticFileProvider says we shouldn't
                    // serve static files
                    return;
                }
            }

            app.UseStaticFiles(staticFileOptions);
        }
    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));
        }

        if (string.IsNullOrEmpty(scriptName))
        {
            throw new ArgumentException("Cannot be null or empty", nameof(scriptName));
        }

        // Start Angular CLI 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 angularCliServerInfoTask = StartAngularCliServerAsync(sourcePath, scriptName, pkgManagerCommand, devServerPort, logger, diagnosticSource, applicationStoppingToken);

        SpaProxyingExtensions.UseProxyToSpaDevelopmentServer(spaBuilder, () =>
        {
            // 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;
            return(angularCliServerInfoTask.WithTimeout(timeout,
                                                        $"The Angular CLI process did not start listening for requests " +
                                                        $"within the timeout period of {timeout.TotalSeconds} seconds. " +
                                                        $"Check the log output for error information."));
        });
    }
        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)
        {
            var sourcePath = spaBuilder.Options.SourcePath;

            if (string.IsNullOrEmpty(sourcePath))
            {
#pragma warning disable CA1303 // Do not pass literals as localized parameters
                throw new InvalidOperationException("Must set ISpaBuilder.Options.SourcePath before calling this method.");
#pragma warning restore CA1303 // Do not pass literals as localized parameters
            }

            if (string.IsNullOrEmpty(npmScriptName))
            {
#pragma warning disable CA1303 // Do not pass literals as localized parameters
                throw new ArgumentException("Cannot be null or empty", nameof(npmScriptName));
#pragma warning restore CA1303 // Do not pass literals as localized parameters
            }

            // Start webpack-dev-server and attach to middleware pipeline
            var appBuilder = spaBuilder.ApplicationBuilder;
            var logger     = LoggerFinder.GetOrCreateLogger(appBuilder, LogCategoryName);

            Task <int> portTask      = null;
            Task <Uri> targetUriTask = null;
            appBuilder.Use(async(context, next) =>
            {
                if (portTask == null)
                {
                    // Get port number of webapp first before we start webpack-dev-server, so that
                    // webpack can use the port number of the webapp for the websocket configuration.
                    var request          = context.Request;
                    int socketPortNumber = request.Host.Port.Value;
                    portTask             = StartWebpackDevServerAsync(sourcePath, npmScriptName, logger, socketPortNumber);
                    // 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 webpack-dev-server server)
                    // - given that, there's no reason to use https, and we couldn't even if we
                    //   wanted to, because in general the webpack-dev-server server has no certificate
#pragma warning disable CA2008 // Do not create tasks without passing a TaskScheduler
                    targetUriTask = portTask.ContinueWith(task =>
                    {
                        // "https" here doesn't work as the webpack-dev-server expects request via "http"
                        Uri uri = new UriBuilder("http", "localhost", task.Result).Uri;
                        return(uri);
                    });
#pragma warning restore CA2008 // Do not create tasks without passing a TaskScheduler
                }

#pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task
                await next.Invoke();
#pragma warning restore CA2007 // Consider calling ConfigureAwait on the awaited task
            });

            SpaProxyingExtensions.UseProxyToSpaDevelopmentServer(spaBuilder, () =>
            {
                // 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;
                return(targetUriTask.WithTimeout(timeout,
                                                 $"The webpack-dev-server did not start listening for requests " +
                                                 $"within the timeout period of {timeout.Seconds} seconds. " +
                                                 $"Check the log output for error information."));
            });
        }
        public static void Attach(IMulitSpaBuilder spaBuilder)
        {
            if (spaBuilder == null)
            {
                throw new ArgumentNullException(nameof(spaBuilder));
            }

            var app     = spaBuilder.ApplicationBuilder;
            var options = spaBuilder.Options;

            var appBuilder = spaBuilder.ApplicationBuilder;
            var logger     = LoggerFinder.GetOrCreateLogger(appBuilder, LogCategoryName);

            logger.LogInformation($"Public path:{options.PublicPath}");

            // 先处理静态资源文件, 未找到的重置为index.html来处理
            app.UseSpaStaticFilesInternal(options.PublicPath, spaBuilder.Options.DistPath,
                                          options.DefaultPageStaticFileOptions,
                                          allowFallbackOnServingWebRootFiles: true);

            // 剩余的请求全部转发到默认页面
            app.Use((context, next) =>
            {
                var loggerFactory = context.RequestServices.GetRequiredService <ILoggerFactory>();
                var localLogger   = loggerFactory.CreateLogger($"{LogCategoryName}.Request");
                localLogger.LogInformation($"Request {context.Request.Path}");
                if (!context.Request.Path.StartsWithSegments(PathString.FromUriComponent(options.PublicPath)))
                {
                    return(next());
                }

                // If we have an Endpoint, then this is a deferred match - just noop.
                if (context.GetEndpoint() != null)
                {
                    return(next());
                }

                context.Request.Path = options.DefaultPage;
                localLogger.LogInformation($"Reset request path to {options.DefaultPage}");
                return(next());
            });

            // Serve it as a static file
            // Developers who need to host more than one SPA with distinct default pages can
            // override the file provider
            app.UseSpaStaticFilesInternal(options.PublicPath, spaBuilder.Options.DistPath,
                                          options.DefaultPageStaticFileOptions ?? new StaticFileOptions(),
                                          allowFallbackOnServingWebRootFiles: true);

            // If the default file didn't get served as a static file (usually because it was not
            // present on disk), the SPA is definitely not going to work.
            app.Use((context, next) =>
            {
                var loggerFactory = context.RequestServices.GetRequiredService <ILoggerFactory>();
                var localLogger   = loggerFactory.CreateLogger($"{LogCategoryName}.2ndRequest");

                if (!context.Request.Path.StartsWithSegments(PathString.FromUriComponent(options.PublicPath)))
                {
                    localLogger.LogInformation("Not public path, Move to next");
                    return(next());
                }

                // If we have an Endpoint, then this is a deferred match - just noop.
                if (context.GetEndpoint() != null)
                {
                    localLogger.LogInformation("context.GetEndpoint() != null, Move to next");
                    return(next());
                }

                var message = "The SPA default page middleware could not return the default page " +
                              $"'{options.DefaultPage}' because it was not found, and no other middleware " +
                              "handled the request.\n";

                // Try to clarify the common scenario where someone runs an application in
                // Production environment without first publishing the whole application
                // or at least building the SPA.
                var hostEnvironment =
                    (IWebHostEnvironment)context.RequestServices.GetService(typeof(IWebHostEnvironment));
                if (hostEnvironment != null && hostEnvironment.IsProduction())
                {
                    message += "Your application is running in Production mode, so make sure it has " +
                               "been published, or that you have built your SPA manually. Alternatively you " +
                               "may wish to switch to the Development environment.\n";
                }

                throw new InvalidOperationException(message);
            });
        }
        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)
        {
            var sourcePath = spaBuilder.Options.SourcePath;

            if (string.IsNullOrEmpty(sourcePath))
            {
#pragma warning disable CA1303 // Do not pass literals as localized parameters
                throw new InvalidOperationException("Must set ISpaBuilder.Options.SourcePath before calling this method.");
#pragma warning restore CA1303 // Do not pass literals as localized parameters
            }

            if (string.IsNullOrEmpty(npmScriptName))
            {
#pragma warning disable CA1303 // Do not pass literals as localized parameters
                throw new ArgumentException("Cannot be null or empty", nameof(npmScriptName));
#pragma warning restore CA1303 // Do not pass literals as localized parameters
            }

            var appBuilder = spaBuilder.ApplicationBuilder;
            var logger     = LoggerFinder.GetOrCreateLogger(appBuilder, LogCategoryName);

            // Clear files in distribution directory aka spaStaticFileOptions.RootPath:
            var spaStaticFileProvider = spaBuilder.ApplicationBuilder.ApplicationServices.GetService(typeof(ISpaStaticFileProvider)) as ISpaStaticFileProvider;
            ClearSpaRootPath(spaStaticFileProvider.FileProvider, logger);

            // Start webpack-dev-server once the application has started
            var        hostApplicationLifetime = spaBuilder.ApplicationBuilder.ApplicationServices.GetService(typeof(IHostApplicationLifetime)) as IHostApplicationLifetime;
            Task <int> portTask         = null;
            Task <Uri> targetUriTask    = null;
            var        socketPortNumber = 0;
            hostApplicationLifetime.ApplicationStarted.Register(() =>
            {
                // When this is called the request pipeline configuration has completed. Only now the addresses
                // at which requests are served are available. We use any address/port combination but use HTTPs
                // if is configured for the project.
                var addressFeature = spaBuilder.ApplicationBuilder.ServerFeatures.Get <IServerAddressesFeature>();
                foreach (var addr in addressFeature.Addresses)
                {
                    var uri          = new Uri(addr);
                    socketPortNumber = uri.Port;
                    if (uri.Scheme == "https")
                    {
                        break;
                    }
                }

                portTask = StartWebpackDevServerAsync(sourcePath, npmScriptName, logger, socketPortNumber);
                // 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 webpack-dev-server)
                // - given that, there's no reason to use https when forwarding the request to the webpack
                //   dev server, and we couldn't even if we wanted to, because in general the webpack-dev-server
                //   has no certificate
#pragma warning disable CA2008 // Do not create tasks without passing a TaskScheduler
                targetUriTask = portTask.ContinueWith(task =>
                {
                    // "https" here doesn't work as the webpack-dev-server expects request via "http"
                    Uri uri = new UriBuilder("http", "localhost", task.Result).Uri;
                    return(uri);
                });
#pragma warning restore CA2008 // Do not create tasks without passing a TaskScheduler
            });

            // Configure proxying. By the time a request comes in, the webpack dev server will be running,
            // so it is fine to configure proxying before the webpack-dev-server has been started.
            SpaProxyingExtensions.UseProxyToSpaDevelopmentServer(spaBuilder, () =>
            {
                // 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;
                return(targetUriTask.WithTimeout(timeout,
                                                 $"The webpack-dev-server did not start listening for requests " +
                                                 $"within the timeout period of {timeout.Seconds} seconds. " +
                                                 $"Check the log output for error information."));
            });
        }