// based on CreateRequestDelegate() https://github.com/aspnet/AspNetCore/blob/master/src/Middleware/StaticFiles/src/StaticFilesEndpointRouteBuilderExtensions.cs#L194
        private static RequestDelegate CreateProxyRequestDelegate(
            IEndpointRouteBuilder endpoints,
            SpaOptions options,
            string npmScript,
            int port = 8080,
            ScriptRunnerType runner = ScriptRunnerType.Npm,
            string regex = VueCliMiddleware.DefaultRegex)
        {
            if (endpoints == null) { throw new ArgumentNullException(nameof(endpoints)); }
            if (options == null) { throw new ArgumentNullException(nameof(options)); }
            if (npmScript == null) { throw new ArgumentNullException(nameof(npmScript)); }

            var app = endpoints.CreateApplicationBuilder();
            app.Use(next => context =>
            {
                // Set endpoint to null so the SPA middleware will handle the request.
                context.SetEndpoint(null);
                return next(context);
            });

            app.UseSpa(opt =>
            {
                if (options != null)
                {
                    opt.Options.DefaultPage = options.DefaultPage;
                    opt.Options.DefaultPageStaticFileOptions = options.DefaultPageStaticFileOptions;
                    opt.Options.SourcePath = options.SourcePath;
                    opt.Options.StartupTimeout = options.StartupTimeout;
                }
                opt.UseVueCli(npmScript, port, runner, regex);
            });

            return app.Build();
        }
        private static async Task <int> StartVueCliServerAsync(
            string sourcePath,
            string npmScriptName,
            ILogger logger,
            int portNumber,
            ScriptRunnerType runner,
            string regex,
            bool forceKill = false,
            bool wsl       = false)
        {
            if (portNumber < 80)
            {
                portNumber = TcpPortFinder.FindAvailablePort();
            }
            else
            {
                // if the port we want to use is occupied, terminate the process utilizing that port.
                // this occurs when "stop" is used from the debugger and the middleware does not have the opportunity to kill the process
                PidUtils.KillPort((ushort)portNumber, forceKill);
            }
            logger.LogInformation($"Starting server on port {portNumber}...");

            var envVars = new Dictionary <string, string>
            {
                { "PORT", portNumber.ToString() },
                { "DEV_SERVER_PORT", portNumber.ToString() }, // vue cli 3 uses --port {number}, included below
                { "BROWSER", "none" },                        // We don't want vue-cli to open its own extra browser window pointing to the internal dev server port
                { "CODESANDBOX_SSE", true.ToString() },       // this will make vue cli use client side HMR inference
            };

            var npmScriptRunner = new ScriptRunner(sourcePath, npmScriptName, $"--port {portNumber:0}", envVars, runner: runner, wsl: wsl);

            AppDomain.CurrentDomain.DomainUnload       += (s, e) => npmScriptRunner?.Kill();
            AppDomain.CurrentDomain.ProcessExit        += (s, e) => npmScriptRunner?.Kill();
            AppDomain.CurrentDomain.UnhandledException += (s, e) => npmScriptRunner?.Kill();
            npmScriptRunner.AttachToLogger(logger);

            using (var stdErrReader = new EventedStreamStringReader(npmScriptRunner.StdErr))
            {
                try
                {
                    // Although the Vue 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(!string.IsNullOrWhiteSpace(regex) ? regex : DefaultRegex, RegexOptions.None, RegexMatchTimeout));
                }
                catch (EndOfStreamException ex)
                {
                    throw new InvalidOperationException(
                              $"The NPM script '{npmScriptName}' exited without indicating that the " +
                              $"server was listening for requests. The error output was: " +
                              $"{stdErrReader.ReadAsString()}", ex);
                }
            }

            return(portNumber);
        }
Пример #3
0
        public ScriptRunner(string workingDirectory, string scriptName, string arguments, IDictionary <string, string> envVars, ScriptRunnerType runner, bool wsl)
        {
            if (string.IsNullOrEmpty(workingDirectory))
            {
                throw new ArgumentException("Cannot be null or empty.", nameof(workingDirectory));
            }

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

            Runner = runner;

            var exeName           = GetExeName();
            var completeArguments = BuildCommand(runner, scriptName, arguments);

            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
            {
                if (wsl)
                {
                    completeArguments = $"{exeName} {completeArguments}";
                    exeName           = "wsl";
                }
                else
                {
                    // On Windows, the NPM executable is a .cmd file, so it can't be executed
                    // directly (except with UseShellExecute=true, but that's no good, because
                    // it prevents capturing stdio). So we need to invoke it via "cmd /c".
                    completeArguments = $"/c {exeName} {completeArguments}";
                    exeName           = "cmd";
                }
            }

            var processStartInfo = new ProcessStartInfo(exeName)
            {
                Arguments              = completeArguments,
                UseShellExecute        = false,
                RedirectStandardInput  = true,
                RedirectStandardOutput = true,
                RedirectStandardError  = true,
                WorkingDirectory       = workingDirectory
            };

            if (envVars != null)
            {
                foreach (var keyValuePair in envVars)
                {
                    processStartInfo.Environment[keyValuePair.Key] = keyValuePair.Value;
                }
            }

            RunnerProcess = LaunchNodeProcess(processStartInfo);

            StdOut = new EventedStreamReader(RunnerProcess.StandardOutput);
            StdErr = new EventedStreamReader(RunnerProcess.StandardError);
        }
 public static IEndpointConventionBuilder MapToVueCliProxy(
     this IEndpointRouteBuilder endpoints,
     SpaOptions options,
     string npmScript        = "serve",
     int port                = 8080,
     ScriptRunnerType runner = ScriptRunnerType.Npm,
     string regex            = VueCliMiddleware.DefaultRegex)
 {
     return(endpoints.MapFallback("{*path}", CreateProxyRequestDelegate(endpoints, options, npmScript, port, runner, regex)));
 }
 public static IEndpointConventionBuilder MapToVueCliProxy(
     this IEndpointRouteBuilder endpoints,
     string sourcePath,
     string npmScript,
     int port = 8080,
     ScriptRunnerType runner = ScriptRunnerType.Npm,
     string regex = VueCliMiddleware.DefaultRegex)
 {
     if (sourcePath == null) { throw new ArgumentNullException(nameof(sourcePath)); }
     return endpoints.MapFallback("{*path}", CreateProxyRequestDelegate(endpoints, new SpaOptions { SourcePath = sourcePath }, npmScript, port, runner, regex));
 }
        public ScriptRunner(string workingDirectory, string scriptName, string arguments, IDictionary <string, string> envVars, ScriptRunnerType runner)
        {
            if (string.IsNullOrEmpty(workingDirectory))
            {
                throw new ArgumentException("Cannot be null or empty.", nameof(workingDirectory));
            }

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

            Runner = runner;

            var npmExe            = GetExeName();
            var completeArguments = $"{GetArgPrefix()}{scriptName} {GetArgSuffix()}{arguments ?? string.Empty}";

            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
            {
                // On Windows, the NPM executable is a .cmd file, so it can't be executed
                // directly (except with UseShellExecute=true, but that's no good, because
                // it prevents capturing stdio). So we need to invoke it via "cmd /c".
                completeArguments = $"/c {npmExe} {completeArguments}";
                npmExe            = "cmd";
            }

            var processStartInfo = new ProcessStartInfo(npmExe)
            {
                Arguments              = completeArguments,
                UseShellExecute        = false,
                RedirectStandardInput  = true,
                RedirectStandardOutput = true,
                RedirectStandardError  = true,
                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);
            StdErr = new EventedStreamReader(process.StandardError);

            // Ensure node process is killed if test process termination is non-graceful.
            ProcessTracker.Add(process);
        }
        private static RequestDelegate CreateProxyRequestDelegate(
            IEndpointRouteBuilder endpoints,
            SpaOptions options,
            string npmScript        = "serve",
            int port                = 8080,
            ScriptRunnerType runner = ScriptRunnerType.Npm,
            string regex            = SpaCliMiddleware.DefaultRegex,
            bool forceKill          = false,
            bool useProxy           = true)
        {
            // based on CreateRequestDelegate() https://github.com/aspnet/AspNetCore/blob/master/src/Middleware/StaticFiles/src/StaticFilesEndpointRouteBuilderExtensions.cs#L194

            if (endpoints == null)
            {
                throw new ArgumentNullException(nameof(endpoints));
            }
            if (options == null)
            {
                throw new ArgumentNullException(nameof(options));
            }
            if (npmScript == null)
            {
                throw new ArgumentNullException(nameof(npmScript));
            }

            var app = endpoints.CreateApplicationBuilder();

            app.Use(next => context =>
            {
                // Set endpoint to null so the SPA middleware will handle the request.
                context.SetEndpoint(null);
                return(next(context));
            });

            app.UseSpa(opt =>
            {
                if (options != null)
                {
                    opt.Options.DefaultPage = options.DefaultPage;
                    opt.Options.DefaultPageStaticFileOptions = options.DefaultPageStaticFileOptions;
                    opt.Options.SourcePath     = options.SourcePath;
                    opt.Options.StartupTimeout = options.StartupTimeout;
                }

                if (!string.IsNullOrWhiteSpace(npmScript))
                {
                    opt.UseSpaCli(npmScript, port, runner, regex, forceKill, useProxy);
                }
            });

            return(app.Build());
        }
Пример #8
0
        public ScriptRunner(string workingDirectory, string scriptName, string arguments, IDictionary <string, string> envVars, ScriptRunnerType runner)
        {
            if (string.IsNullOrEmpty(workingDirectory))
            {
                throw new ArgumentException("Cannot be null or empty.", nameof(workingDirectory));
            }

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

            Runner = runner;

            var npmExe            = GetExeName();
            var completeArguments = $"{GetArgPrefix()}{scriptName} {GetArgSuffix()}{arguments ?? string.Empty}";

            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
            {
                // 在 Windows 上, NPM 可执行文件是一个. cmd 文件, 因此无法执行
                // 直接 (除了与 Usseshelrex否 = true, 但这是没有好处的, 因为
                // 它可以防止捕获 stdio)。因此, 我们需要通过 "cmd/c" 调用它。
                npmExe            = "cmd";
                completeArguments = $"/c npm {completeArguments}";
            }

            var processStartInfo = new ProcessStartInfo(npmExe)
            {
                Arguments              = completeArguments,
                UseShellExecute        = false,
                RedirectStandardInput  = true,
                RedirectStandardOutput = true,
                RedirectStandardError  = true,
                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);
            StdErr = new EventedStreamReader(process.StandardError);
        }
 public static IEndpointConventionBuilder MapToVueCliProxy(
     this IEndpointRouteBuilder endpoints,
     string pattern,
     SpaOptions options,
     string npmScript        = "serve",
     int port                = 8080,
     ScriptRunnerType runner = ScriptRunnerType.Npm,
     string regex            = VueCliMiddleware.DefaultRegex)
 {
     if (pattern == null)
     {
         throw new ArgumentNullException(nameof(pattern));
     }
     return(endpoints.MapFallback(pattern, CreateProxyRequestDelegate(endpoints, options, npmScript, port, runner, regex)));
 }
Пример #10
0
        private static TimeSpan RegexMatchTimeout = TimeSpan.FromMinutes(5); // This is a development-time only feature, so a very long timeout is fine

        public static void Attach(
            ISpaBuilder spaBuilder,
            string scriptName, int port = 8080, ScriptRunnerType runner = ScriptRunnerType.Npm, string regex = DefaultRegex, bool forceKill = false, bool useProxy = true)
        {
            string sourcePath = spaBuilder.Options.SourcePath;

            Console.WriteLine("sourcePath", sourcePath);
            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 vue-cli and attach to middleware pipeline
            var appBuilder = spaBuilder.ApplicationBuilder;
            var logger     = LoggerFinder.GetOrCreateLogger(appBuilder, LogCategoryName);
            var portTask   = StartSpaCliServerAsync(sourcePath, scriptName, logger, port, runner, regex, forceKill);

            if (!useProxy)
            {
                return;
            }

            // 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 vue-cli server)
            // - given that, there's no reason to use https, and we couldn't even if we
            //   wanted to, because in general the vue-cli server has no certificate
            var targetUriTask = portTask.ContinueWith(
                task => new UriBuilder("http", "localhost", task.Result).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 svelte-cli server did not start listening for requests " +
                                                 $"within the timeout period of {timeout.Seconds} seconds. " +
                                                 $"Check the log output for error information."));
            });
        }
Пример #11
0
        private static async Task <int> StartSvelteServerAsync(
            string sourcePath,
            string npmScriptName,
            ILogger logger,
            int portNumber,
            ScriptRunnerType runner,
            string regex)
        {
            if (portNumber < 80)
            {
                portNumber = TcpPortFinder.FindAvailablePort();
            }
            logger.LogInformation($"Starting server on port {portNumber}...");

            var envVars = new Dictionary <string, string>
            {
                { "PORT", portNumber.ToString() },
                { "DEV_SERVER_PORT", portNumber.ToString() }, // Svelte uses --port {number}, included below
                { "BROWSER", "none" },                        // We don't want Svelte to open its own extra browser window pointing to the internal dev server port
            };
            var npmScriptRunner = new ScriptRunner(sourcePath, npmScriptName, $"--port {portNumber:0}", envVars, runner: runner);

            npmScriptRunner.AttachToLogger(logger);

            using (var stdErrReader = new EventedStreamStringReader(npmScriptRunner.StdErr))
            {
                try
                {
                    // Although the Svelte 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(!string.IsNullOrWhiteSpace(regex) ? regex : DefaultRegex, RegexOptions.None, RegexMatchTimeout));
                }
                catch (EndOfStreamException ex)
                {
                    throw new InvalidOperationException(
                              $"The NPM script '{npmScriptName}' exited without indicating that the " +
                              $"server was listening for requests. The error output was: " +
                              $"{stdErrReader.ReadAsString()}", ex);
                }
            }

            return(portNumber);
        }
        private static TimeSpan RegexMatchTimeout = TimeSpan.FromMinutes(5); // 这是仅开发时间的功能, 因此很长时间的超时是可以的

        public static void Attach(
            ISpaBuilder spaBuilder,
            string scriptName, int port = 0, ScriptRunnerType runner = ScriptRunnerType.Npm, string regex = DefaultRegex)
        {
            var sourcePath = spaBuilder.Options.SourcePath;

            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));
            }

            // 启动vue-cli并连接到中间件管道
            var appBuilder = spaBuilder.ApplicationBuilder;
            var logger     = LoggerFinder.GetOrCreateLogger(appBuilder, LogCategoryName);
            var portTask   = StartVueCliServerAsync(sourcePath, scriptName, logger, port, runner, regex);

            // 我们代理的所有内容都硬编码为目标http://localhost,因为:
            // - 请求总是来自本地机器(我们不接受远程
            //   直接转到vue-cli服务器的请求)
            // - 鉴于此,没有理由使用https,即使我们也不行
            //   想要,因为通常vue-cli服务器没有证书
            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-cli server did not start listening for requests " +
                                                 $"within the timeout period of {timeout.Seconds} seconds. " +
                                                 $"Check the log output for error information."));
            });
        }
        /// <summary>
        /// Handles requests by passing them through to an instance of the vue-cli server.
        /// This means you can always serve up-to-date CLI-built resources without having
        /// to run the vue-cli server manually.
        ///
        /// This feature should only be used in development. For production deployments, be
        /// sure not to enable the vue-cli 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 server.</param>
        /// <param name="port">Specify vue cli server port number. If &lt; 80, uses random port. </param>
        /// <param name="runner">Specify the runner, Npm and Yarn are valid options. Yarn support is HIGHLY experimental.</param>
        /// <param name="regex">Specify a custom regex string to search for in the log indicating proxied server is running. VueCli: "running at", QuasarCli: "Compiled successfully"</param>
        public static void UseVueCli(
            this ISpaBuilder spaBuilder,
            string npmScript        = "serve",
            int port                = 8080,
            ScriptRunnerType runner = ScriptRunnerType.Npm,
            string regex            = VueCliMiddleware.DefaultRegex)
        {
            if (spaBuilder == null)
            {
                throw new ArgumentNullException(nameof(spaBuilder));
            }

            var spaOptions = spaBuilder.Options;

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

            VueCliMiddleware.Attach(spaBuilder, npmScript, port, runner: runner, regex: regex);
        }
        private static async Task <int> StartVueCliServerAsync(
            string sourcePath, string npmScriptName, ILogger logger, int portNumber, ScriptRunnerType runner, string regex)
        {
            if (portNumber < 80)
            {
                portNumber = TcpPortFinder.FindAvailablePort();
            }
            logger.LogInformation($"Starting server on port {portNumber}...");

            var envVars = new Dictionary <string, string>
            {
                { "PORT", portNumber.ToString() },
                { "DEV_SERVER_PORT", portNumber.ToString() }, // vue cli 3使用--port {number},包含在下面
                { "BROWSER", "none" },                        // 我们不希望vue-cli打开指向内部开发服务器端口的额外浏览器窗口
            };
            var npmScriptRunner = new ScriptRunner(sourcePath, npmScriptName, $"--port {portNumber:0}", envVars, runner: runner);

            npmScriptRunner.AttachToLogger(logger);

            using (var stdErrReader = new EventedStreamStringReader(npmScriptRunner.StdErr))
            {
                try
                {
                    // 虽然Vue dev服务器最终可能告诉我们它正在监听的URL,
                    // 在完成编译之前不会这样做,即使那时候也是如此
                    // 没有编译器警告。 因此,不要等待,尽快考虑好
                    // 当它开始侦听请求时
                    await npmScriptRunner.StdOut.WaitForMatch(new Regex(!string.IsNullOrWhiteSpace(regex) ? regex : DefaultRegex, RegexOptions.None, RegexMatchTimeout));
                }
                catch (EndOfStreamException ex)
                {
                    throw new InvalidOperationException(
                              $"The NPM script '{npmScriptName}' exited without indicating that the " +
                              $"server was listening for requests. The error output was: " +
                              $"{stdErrReader.ReadAsString()}", ex);
                }
            }

            return(portNumber);
        }
Пример #15
0
 public static IEndpointConventionBuilder MapToVueCliProxy(
     this IEndpointRouteBuilder endpoints,
     string pattern,
     string sourcePath,
     string npmScript        = "serve",
     int port                = 8080,
     bool https              = false,
     ScriptRunnerType runner = ScriptRunnerType.Npm,
     string regex            = VueCliMiddleware.DefaultRegex,
     bool forceKill          = false)
 {
     if (pattern == null)
     {
         throw new ArgumentNullException(nameof(pattern));
     }
     if (sourcePath == null)
     {
         throw new ArgumentNullException(nameof(sourcePath));
     }
     return(endpoints.MapFallback(pattern, CreateProxyRequestDelegate(endpoints, new SpaOptions {
         SourcePath = sourcePath
     }, npmScript, port, https, runner, regex, forceKill)));
 }
Пример #16
0
        private static string BuildCommand(ScriptRunnerType runner, string scriptName, string arguments)
        {
            var command = new StringBuilder();

            if (runner == ScriptRunnerType.Npm)
            {
                command.Append("run ");
            }

            command.Append(scriptName);
            command.Append(' ');

            if (runner == ScriptRunnerType.Npm)
            {
                command.Append("-- ");
            }

            if (!string.IsNullOrWhiteSpace(arguments))
            {
                command.Append(arguments);
            }
            return(command.ToString());
        }