// 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); }
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()); }
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))); }
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.")); }); }
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 < 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); }
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))); }
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()); }