From 751882cf3c45d0eb982f244d59bfc758b4b6c17b Mon Sep 17 00:00:00 2001 From: Justin Robb Date: Wed, 31 Jul 2019 12:56:08 -0700 Subject: [PATCH 1/5] Add optional packageManagerName to allow alternative package managers --- .../src/AngularCli/AngularCliBuilder.cs | 9 +++-- .../src/AngularCli/AngularCliMiddleware.cs | 9 ++--- ...NpmScriptRunner.cs => NodeScriptRunner.cs} | 33 +++++++++++-------- .../ReactDevelopmentServerMiddleware.cs | 9 ++--- ...ctDevelopmentServerMiddlewareExtensions.cs | 2 +- .../SpaServices.Extensions/src/SpaOptions.cs | 28 ++++++++++++++-- 6 files changed, 61 insertions(+), 29 deletions(-) rename src/Middleware/SpaServices.Extensions/src/Npm/{NpmScriptRunner.cs => NodeScriptRunner.cs} (77%) diff --git a/src/Middleware/SpaServices.Extensions/src/AngularCli/AngularCliBuilder.cs b/src/Middleware/SpaServices.Extensions/src/AngularCli/AngularCliBuilder.cs index 61dedd350a..0a6a320193 100644 --- a/src/Middleware/SpaServices.Extensions/src/AngularCli/AngularCliBuilder.cs +++ b/src/Middleware/SpaServices.Extensions/src/AngularCli/AngularCliBuilder.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// 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; @@ -40,20 +40,23 @@ namespace Microsoft.AspNetCore.SpaServices.AngularCli /// public async Task Build(ISpaBuilder spaBuilder) { + var pkgManagerName = spaBuilder.Options.PackageManagerName; 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( + var npmScriptRunner = new NodeScriptRunner( sourcePath, _npmScriptName, "--watch", - null); + null, + pkgManagerName); npmScriptRunner.AttachToLogger(logger); using (var stdOutReader = new EventedStreamStringReader(npmScriptRunner.StdOut)) diff --git a/src/Middleware/SpaServices.Extensions/src/AngularCli/AngularCliMiddleware.cs b/src/Middleware/SpaServices.Extensions/src/AngularCli/AngularCliMiddleware.cs index 9090f7738b..1f1ba8cdc1 100644 --- a/src/Middleware/SpaServices.Extensions/src/AngularCli/AngularCliMiddleware.cs +++ b/src/Middleware/SpaServices.Extensions/src/AngularCli/AngularCliMiddleware.cs @@ -25,6 +25,7 @@ namespace Microsoft.AspNetCore.SpaServices.AngularCli ISpaBuilder spaBuilder, string npmScriptName) { + var pkgManagerName = spaBuilder.Options.PackageManagerName; var sourcePath = spaBuilder.Options.SourcePath; if (string.IsNullOrEmpty(sourcePath)) { @@ -39,7 +40,7 @@ namespace Microsoft.AspNetCore.SpaServices.AngularCli // Start Angular CLI and attach to middleware pipeline var appBuilder = spaBuilder.ApplicationBuilder; var logger = LoggerFinder.GetOrCreateLogger(appBuilder, LogCategoryName); - var angularCliServerInfoTask = StartAngularCliServerAsync(sourcePath, npmScriptName, logger); + var angularCliServerInfoTask = StartAngularCliServerAsync(sourcePath, npmScriptName, pkgManagerName, logger); // Everything we proxy is hardcoded to target http://localhost because: // - the requests are always from the local machine (we're not accepting remote @@ -62,13 +63,13 @@ namespace Microsoft.AspNetCore.SpaServices.AngularCli } private static async Task StartAngularCliServerAsync( - string sourcePath, string npmScriptName, ILogger logger) + string sourcePath, string npmScriptName, string pkgManagerName, ILogger logger) { var portNumber = TcpPortFinder.FindAvailablePort(); logger.LogInformation($"Starting @angular/cli on port {portNumber}..."); - var npmScriptRunner = new NpmScriptRunner( - sourcePath, npmScriptName, $"--port {portNumber}", null); + var npmScriptRunner = new NodeScriptRunner( + sourcePath, npmScriptName, $"--port {portNumber}", null, pkgManagerName); npmScriptRunner.AttachToLogger(logger); Match openBrowserLine; diff --git a/src/Middleware/SpaServices.Extensions/src/Npm/NpmScriptRunner.cs b/src/Middleware/SpaServices.Extensions/src/Npm/NodeScriptRunner.cs similarity index 77% rename from src/Middleware/SpaServices.Extensions/src/Npm/NpmScriptRunner.cs rename to src/Middleware/SpaServices.Extensions/src/Npm/NodeScriptRunner.cs index 378ec5f9fa..86e6497f19 100644 --- a/src/Middleware/SpaServices.Extensions/src/Npm/NpmScriptRunner.cs +++ b/src/Middleware/SpaServices.Extensions/src/Npm/NodeScriptRunner.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// 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.Extensions.Logging; @@ -16,14 +16,14 @@ namespace Microsoft.AspNetCore.NodeServices.Npm /// Executes the script entries defined in a package.json file, /// capturing any output written to stdio. /// - internal class NpmScriptRunner + internal class NodeScriptRunner { public EventedStreamReader StdOut { get; } public EventedStreamReader StdErr { get; } private static Regex AnsiColorRegex = new Regex("\x001b\\[[0-9;]*m", RegexOptions.None, TimeSpan.FromSeconds(1)); - public NpmScriptRunner(string workingDirectory, string scriptName, string arguments, IDictionary envVars) + public NodeScriptRunner(string workingDirectory, string scriptName, string arguments, IDictionary envVars, string pkgManagerName) { if (string.IsNullOrEmpty(workingDirectory)) { @@ -35,18 +35,23 @@ namespace Microsoft.AspNetCore.NodeServices.Npm throw new ArgumentException("Cannot be null or empty.", nameof(scriptName)); } - var npmExe = "npm"; + if (string.IsNullOrEmpty(pkgManagerName)) + { + throw new ArgumentException("Cannot be null or empty.", nameof(pkgManagerName)); + } + + var exeToRun = pkgManagerName; var completeArguments = $"run {scriptName} -- {arguments ?? string.Empty}"; if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - // On Windows, the NPM executable is a .cmd file, so it can't be executed + // On Windows, the node 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". - npmExe = "cmd"; - completeArguments = $"/c npm {completeArguments}"; + exeToRun = "cmd"; + completeArguments = $"/c {pkgManagerName} {completeArguments}"; } - var processStartInfo = new ProcessStartInfo(npmExe) + var processStartInfo = new ProcessStartInfo(exeToRun) { Arguments = completeArguments, UseShellExecute = false, @@ -64,19 +69,19 @@ namespace Microsoft.AspNetCore.NodeServices.Npm } } - var process = LaunchNodeProcess(processStartInfo); + var process = LaunchNodeProcess(processStartInfo, pkgManagerName); StdOut = new EventedStreamReader(process.StandardOutput); StdErr = new EventedStreamReader(process.StandardError); } public void AttachToLogger(ILogger logger) { - // When the NPM task emits complete lines, pass them through to the real logger + // When the node task emits complete lines, pass them through to the real logger StdOut.OnReceivedLine += line => { if (!string.IsNullOrWhiteSpace(line)) { - // NPM tasks commonly emit ANSI colors, but it wouldn't make sense to forward + // Node tasks commonly emit ANSI colors, but it wouldn't make sense to forward // those to loggers (because a logger isn't necessarily any kind of terminal) logger.LogInformation(StripAnsiColors(line)); } @@ -106,7 +111,7 @@ namespace Microsoft.AspNetCore.NodeServices.Npm private static string StripAnsiColors(string line) => AnsiColorRegex.Replace(line, string.Empty); - private static Process LaunchNodeProcess(ProcessStartInfo startInfo) + private static Process LaunchNodeProcess(ProcessStartInfo startInfo, string commandName) { try { @@ -119,8 +124,8 @@ namespace Microsoft.AspNetCore.NodeServices.Npm } catch (Exception ex) { - var message = $"Failed to start 'npm'. To resolve this:.\n\n" - + "[1] Ensure that 'npm' is installed and can be found in one of the PATH directories.\n" + var message = $"Failed to start '{commandName}'. To resolve this:.\n\n" + + $"[1] Ensure that '{commandName}' is installed and can be found in one of the PATH directories.\n" + $" Current PATH enviroment variable is: { Environment.GetEnvironmentVariable("PATH") }\n" + " Make sure the executable is in one of those directories, or update your PATH.\n\n" + "[2] See the InnerException for further details of the cause."; diff --git a/src/Middleware/SpaServices.Extensions/src/ReactDevelopmentServer/ReactDevelopmentServerMiddleware.cs b/src/Middleware/SpaServices.Extensions/src/ReactDevelopmentServer/ReactDevelopmentServerMiddleware.cs index 78a7b4f03f..4dfe28457d 100644 --- a/src/Middleware/SpaServices.Extensions/src/ReactDevelopmentServer/ReactDevelopmentServerMiddleware.cs +++ b/src/Middleware/SpaServices.Extensions/src/ReactDevelopmentServer/ReactDevelopmentServerMiddleware.cs @@ -24,6 +24,7 @@ namespace Microsoft.AspNetCore.SpaServices.ReactDevelopmentServer ISpaBuilder spaBuilder, string npmScriptName) { + var pkgManagerName = spaBuilder.Options.PackageManagerName; var sourcePath = spaBuilder.Options.SourcePath; if (string.IsNullOrEmpty(sourcePath)) { @@ -38,7 +39,7 @@ namespace Microsoft.AspNetCore.SpaServices.ReactDevelopmentServer // 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 = StartCreateReactAppServerAsync(sourcePath, npmScriptName, pkgManagerName, logger); // Everything we proxy is hardcoded to target http://localhost because: // - the requests are always from the local machine (we're not accepting remote @@ -61,7 +62,7 @@ namespace Microsoft.AspNetCore.SpaServices.ReactDevelopmentServer } private static async Task StartCreateReactAppServerAsync( - string sourcePath, string npmScriptName, ILogger logger) + string sourcePath, string npmScriptName, string pkgManagerName, ILogger logger) { var portNumber = TcpPortFinder.FindAvailablePort(); logger.LogInformation($"Starting create-react-app server on port {portNumber}..."); @@ -71,8 +72,8 @@ namespace Microsoft.AspNetCore.SpaServices.ReactDevelopmentServer { "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 }; - var npmScriptRunner = new NpmScriptRunner( - sourcePath, npmScriptName, null, envVars); + var npmScriptRunner = new NodeScriptRunner( + sourcePath, npmScriptName, null, envVars, pkgManagerName); npmScriptRunner.AttachToLogger(logger); using (var stdErrReader = new EventedStreamStringReader(npmScriptRunner.StdErr)) diff --git a/src/Middleware/SpaServices.Extensions/src/ReactDevelopmentServer/ReactDevelopmentServerMiddlewareExtensions.cs b/src/Middleware/SpaServices.Extensions/src/ReactDevelopmentServer/ReactDevelopmentServerMiddlewareExtensions.cs index f58a6d1a9d..346e839046 100644 --- a/src/Middleware/SpaServices.Extensions/src/ReactDevelopmentServer/ReactDevelopmentServerMiddlewareExtensions.cs +++ b/src/Middleware/SpaServices.Extensions/src/ReactDevelopmentServer/ReactDevelopmentServerMiddlewareExtensions.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// 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; diff --git a/src/Middleware/SpaServices.Extensions/src/SpaOptions.cs b/src/Middleware/SpaServices.Extensions/src/SpaOptions.cs index b2823396dc..5b3d06a94d 100644 --- a/src/Middleware/SpaServices.Extensions/src/SpaOptions.cs +++ b/src/Middleware/SpaServices.Extensions/src/SpaOptions.cs @@ -1,11 +1,10 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// 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 System; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.FileProviders; -using System; namespace Microsoft.AspNetCore.SpaServices { @@ -15,6 +14,7 @@ namespace Microsoft.AspNetCore.SpaServices public class SpaOptions { private PathString _defaultPage = "/index.html"; + private string _defaultPackageManagerName = "npm"; /// /// Constructs a new instance of . @@ -30,6 +30,7 @@ namespace Microsoft.AspNetCore.SpaServices internal SpaOptions(SpaOptions copyFromOptions) { _defaultPage = copyFromOptions.DefaultPage; + _defaultPackageManagerName = copyFromOptions.PackageManagerName; DefaultPageStaticFileOptions = copyFromOptions.DefaultPageStaticFileOptions; SourcePath = copyFromOptions.SourcePath; } @@ -69,6 +70,27 @@ namespace Microsoft.AspNetCore.SpaServices /// public string SourcePath { get; set; } + /// + /// Gets or sets the name of the package manager executible, (e.g npm, + /// yarn) to run the SPA. + /// + /// If not set, npm will be assumed as the default package manager + /// executable + /// + public string PackageManagerName + { + get => _defaultPackageManagerName; + set + { + if (string.IsNullOrEmpty(value)) + { + throw new ArgumentException($"The value for {nameof(PackageManagerName)} cannot be null or empty."); + } + + _defaultPackageManagerName = value; + } + } + /// /// Gets or sets the maximum duration that a request will wait for the SPA /// to become ready to serve to the client. From 7c7dd6569f8eabc3586d9a0aed296e46c63fcc98 Mon Sep 17 00:00:00 2001 From: Justin Robb Date: Wed, 31 Jul 2019 13:15:07 -0700 Subject: [PATCH 2/5] Renaming some SPA Middleware variables to be package manager agnostic --- .../src/AngularCli/AngularCliBuilder.cs | 29 +++++++++---------- .../src/AngularCli/AngularCliMiddleware.cs | 22 +++++++------- .../AngularCliMiddlewareExtensions.cs | 8 ++--- .../ReactDevelopmentServerMiddleware.cs | 22 +++++++------- ...ctDevelopmentServerMiddlewareExtensions.cs | 6 ++-- 5 files changed, 43 insertions(+), 44 deletions(-) diff --git a/src/Middleware/SpaServices.Extensions/src/AngularCli/AngularCliBuilder.cs b/src/Middleware/SpaServices.Extensions/src/AngularCli/AngularCliBuilder.cs index 0a6a320193..b6cc1f18c6 100644 --- a/src/Middleware/SpaServices.Extensions/src/AngularCli/AngularCliBuilder.cs +++ b/src/Middleware/SpaServices.Extensions/src/AngularCli/AngularCliBuilder.cs @@ -21,20 +21,20 @@ namespace Microsoft.AspNetCore.SpaServices.AngularCli { private static TimeSpan RegexMatchTimeout = TimeSpan.FromSeconds(5); // This is a development-time only feature, so a very long timeout is fine - private readonly string _npmScriptName; + private readonly string _scriptName; /// /// Constructs an instance of . /// - /// The name of the script in your package.json file that builds the server-side bundle for your Angular application. - public AngularCliBuilder(string npmScript) + /// The name of the script in your package.json file that builds the server-side bundle for your Angular application. + public AngularCliBuilder(string scriptName) { - if (string.IsNullOrEmpty(npmScript)) + if (string.IsNullOrEmpty(scriptName)) { - throw new ArgumentException("Cannot be null or empty.", nameof(npmScript)); + throw new ArgumentException("Cannot be null or empty.", nameof(scriptName)); } - _npmScriptName = npmScript; + _scriptName = scriptName; } /// @@ -47,37 +47,36 @@ namespace Microsoft.AspNetCore.SpaServices.AngularCli 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 NodeScriptRunner( + var scriptRunner = new NodeScriptRunner( sourcePath, - _npmScriptName, + _scriptName, "--watch", null, pkgManagerName); - npmScriptRunner.AttachToLogger(logger); + scriptRunner.AttachToLogger(logger); - using (var stdOutReader = new EventedStreamStringReader(npmScriptRunner.StdOut)) - using (var stdErrReader = new EventedStreamStringReader(npmScriptRunner.StdErr)) + using (var stdOutReader = new EventedStreamStringReader(scriptRunner.StdOut)) + using (var stdErrReader = new EventedStreamStringReader(scriptRunner.StdErr)) { try { - await npmScriptRunner.StdOut.WaitForMatch( + await scriptRunner.StdOut.WaitForMatch( new Regex("Date", RegexOptions.None, RegexMatchTimeout)); } catch (EndOfStreamException ex) { throw new InvalidOperationException( - $"The NPM script '{_npmScriptName}' exited without indicating success.\n" + + $"The {pkgManagerName} 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 NPM script '{_npmScriptName}' timed out without indicating success. " + + $"The {pkgManagerName} script '{_scriptName}' timed out without indicating success. " + $"Output was: {stdOutReader.ReadAsString()}\n" + $"Error output was: {stdErrReader.ReadAsString()}", ex); } diff --git a/src/Middleware/SpaServices.Extensions/src/AngularCli/AngularCliMiddleware.cs b/src/Middleware/SpaServices.Extensions/src/AngularCli/AngularCliMiddleware.cs index 1f1ba8cdc1..dfddd1800a 100644 --- a/src/Middleware/SpaServices.Extensions/src/AngularCli/AngularCliMiddleware.cs +++ b/src/Middleware/SpaServices.Extensions/src/AngularCli/AngularCliMiddleware.cs @@ -23,7 +23,7 @@ namespace Microsoft.AspNetCore.SpaServices.AngularCli public static void Attach( ISpaBuilder spaBuilder, - string npmScriptName) + string scriptName) { var pkgManagerName = spaBuilder.Options.PackageManagerName; var sourcePath = spaBuilder.Options.SourcePath; @@ -32,15 +32,15 @@ namespace Microsoft.AspNetCore.SpaServices.AngularCli throw new ArgumentException("Cannot be null or empty", nameof(sourcePath)); } - if (string.IsNullOrEmpty(npmScriptName)) + if (string.IsNullOrEmpty(scriptName)) { - throw new ArgumentException("Cannot be null or empty", nameof(npmScriptName)); + throw new ArgumentException("Cannot be null or empty", nameof(scriptName)); } // Start Angular CLI and attach to middleware pipeline var appBuilder = spaBuilder.ApplicationBuilder; var logger = LoggerFinder.GetOrCreateLogger(appBuilder, LogCategoryName); - var angularCliServerInfoTask = StartAngularCliServerAsync(sourcePath, npmScriptName, pkgManagerName, logger); + var angularCliServerInfoTask = StartAngularCliServerAsync(sourcePath, scriptName, pkgManagerName, logger); // Everything we proxy is hardcoded to target http://localhost because: // - the requests are always from the local machine (we're not accepting remote @@ -63,27 +63,27 @@ namespace Microsoft.AspNetCore.SpaServices.AngularCli } private static async Task StartAngularCliServerAsync( - string sourcePath, string npmScriptName, string pkgManagerName, ILogger logger) + string sourcePath, string scriptName, string pkgManagerName, ILogger logger) { var portNumber = TcpPortFinder.FindAvailablePort(); logger.LogInformation($"Starting @angular/cli on port {portNumber}..."); - var npmScriptRunner = new NodeScriptRunner( - sourcePath, npmScriptName, $"--port {portNumber}", null, pkgManagerName); - npmScriptRunner.AttachToLogger(logger); + var scriptRunner = new NodeScriptRunner( + sourcePath, scriptName, $"--port {portNumber}", null, pkgManagerName); + scriptRunner.AttachToLogger(logger); Match openBrowserLine; - using (var stdErrReader = new EventedStreamStringReader(npmScriptRunner.StdErr)) + using (var stdErrReader = new EventedStreamStringReader(scriptRunner.StdErr)) { try { - openBrowserLine = await npmScriptRunner.StdOut.WaitForMatch( + openBrowserLine = await scriptRunner.StdOut.WaitForMatch( new Regex("open your browser on (http\\S+)", RegexOptions.None, RegexMatchTimeout)); } catch (EndOfStreamException ex) { throw new InvalidOperationException( - $"The NPM script '{npmScriptName}' exited without indicating that the " + + $"The {pkgManagerName} script '{scriptName}' exited without indicating that the " + $"Angular CLI was listening for requests. The error output was: " + $"{stdErrReader.ReadAsString()}", ex); } diff --git a/src/Middleware/SpaServices.Extensions/src/AngularCli/AngularCliMiddlewareExtensions.cs b/src/Middleware/SpaServices.Extensions/src/AngularCli/AngularCliMiddlewareExtensions.cs index 28e63c8e35..59f8ee5046 100644 --- a/src/Middleware/SpaServices.Extensions/src/AngularCli/AngularCliMiddlewareExtensions.cs +++ b/src/Middleware/SpaServices.Extensions/src/AngularCli/AngularCliMiddlewareExtensions.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// 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; @@ -20,10 +20,10 @@ namespace Microsoft.AspNetCore.SpaServices.AngularCli /// sure not to enable the Angular CLI server. /// /// The . - /// The name of the script in your package.json file that launches the Angular CLI process. + /// The name of the script in your package.json file that launches the Angular CLI process. public static void UseAngularCliServer( this ISpaBuilder spaBuilder, - string npmScript) + string scriptName) { if (spaBuilder == null) { @@ -37,7 +37,7 @@ namespace Microsoft.AspNetCore.SpaServices.AngularCli throw new InvalidOperationException($"To use {nameof(UseAngularCliServer)}, you must supply a non-empty value for the {nameof(SpaOptions.SourcePath)} property of {nameof(SpaOptions)} when calling {nameof(SpaApplicationBuilderExtensions.UseSpa)}."); } - AngularCliMiddleware.Attach(spaBuilder, npmScript); + AngularCliMiddleware.Attach(spaBuilder, scriptName); } } } diff --git a/src/Middleware/SpaServices.Extensions/src/ReactDevelopmentServer/ReactDevelopmentServerMiddleware.cs b/src/Middleware/SpaServices.Extensions/src/ReactDevelopmentServer/ReactDevelopmentServerMiddleware.cs index 4dfe28457d..e7ab1b3dea 100644 --- a/src/Middleware/SpaServices.Extensions/src/ReactDevelopmentServer/ReactDevelopmentServerMiddleware.cs +++ b/src/Middleware/SpaServices.Extensions/src/ReactDevelopmentServer/ReactDevelopmentServerMiddleware.cs @@ -22,7 +22,7 @@ namespace Microsoft.AspNetCore.SpaServices.ReactDevelopmentServer public static void Attach( ISpaBuilder spaBuilder, - string npmScriptName) + string scriptName) { var pkgManagerName = spaBuilder.Options.PackageManagerName; var sourcePath = spaBuilder.Options.SourcePath; @@ -31,15 +31,15 @@ namespace Microsoft.AspNetCore.SpaServices.ReactDevelopmentServer throw new ArgumentException("Cannot be null or empty", nameof(sourcePath)); } - if (string.IsNullOrEmpty(npmScriptName)) + if (string.IsNullOrEmpty(scriptName)) { - throw new ArgumentException("Cannot be null or empty", nameof(npmScriptName)); + throw new ArgumentException("Cannot be null or empty", nameof(scriptName)); } // Start create-react-app and attach to middleware pipeline var appBuilder = spaBuilder.ApplicationBuilder; var logger = LoggerFinder.GetOrCreateLogger(appBuilder, LogCategoryName); - var portTask = StartCreateReactAppServerAsync(sourcePath, npmScriptName, pkgManagerName, logger); + var portTask = StartCreateReactAppServerAsync(sourcePath, scriptName, pkgManagerName, logger); // Everything we proxy is hardcoded to target http://localhost because: // - the requests are always from the local machine (we're not accepting remote @@ -62,7 +62,7 @@ namespace Microsoft.AspNetCore.SpaServices.ReactDevelopmentServer } private static async Task StartCreateReactAppServerAsync( - string sourcePath, string npmScriptName, string pkgManagerName, ILogger logger) + string sourcePath, string scriptName, string pkgManagerName, ILogger logger) { var portNumber = TcpPortFinder.FindAvailablePort(); logger.LogInformation($"Starting create-react-app server on port {portNumber}..."); @@ -72,11 +72,11 @@ namespace Microsoft.AspNetCore.SpaServices.ReactDevelopmentServer { "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 }; - var npmScriptRunner = new NodeScriptRunner( - sourcePath, npmScriptName, null, envVars, pkgManagerName); - npmScriptRunner.AttachToLogger(logger); + var scriptRunner = new NodeScriptRunner( + sourcePath, scriptName, null, envVars, pkgManagerName); + scriptRunner.AttachToLogger(logger); - using (var stdErrReader = new EventedStreamStringReader(npmScriptRunner.StdErr)) + using (var stdErrReader = new EventedStreamStringReader(scriptRunner.StdErr)) { try { @@ -84,13 +84,13 @@ namespace Microsoft.AspNetCore.SpaServices.ReactDevelopmentServer // 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( + await scriptRunner.StdOut.WaitForMatch( new Regex("Starting the development server", RegexOptions.None, RegexMatchTimeout)); } catch (EndOfStreamException ex) { throw new InvalidOperationException( - $"The NPM script '{npmScriptName}' exited without indicating that the " + + $"The {pkgManagerName} script '{scriptName}' exited without indicating that the " + $"create-react-app server was listening for requests. The error output was: " + $"{stdErrReader.ReadAsString()}", ex); } diff --git a/src/Middleware/SpaServices.Extensions/src/ReactDevelopmentServer/ReactDevelopmentServerMiddlewareExtensions.cs b/src/Middleware/SpaServices.Extensions/src/ReactDevelopmentServer/ReactDevelopmentServerMiddlewareExtensions.cs index 346e839046..37532329ee 100644 --- a/src/Middleware/SpaServices.Extensions/src/ReactDevelopmentServer/ReactDevelopmentServerMiddlewareExtensions.cs +++ b/src/Middleware/SpaServices.Extensions/src/ReactDevelopmentServer/ReactDevelopmentServerMiddlewareExtensions.cs @@ -20,10 +20,10 @@ namespace Microsoft.AspNetCore.SpaServices.ReactDevelopmentServer /// 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. + /// 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) + string scriptName) { if (spaBuilder == null) { @@ -37,7 +37,7 @@ namespace Microsoft.AspNetCore.SpaServices.ReactDevelopmentServer 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); + ReactDevelopmentServerMiddleware.Attach(spaBuilder, scriptName); } } } From c3983614121adb3ad7133c18a129048ddc5acce4 Mon Sep 17 00:00:00 2001 From: Justin Robb Date: Thu, 1 Aug 2019 10:14:51 -0700 Subject: [PATCH 3/5] Renaming packageManagerCommand variables, fixing comment --- .../src/AngularCli/AngularCliBuilder.cs | 8 ++++---- .../src/AngularCli/AngularCliMiddleware.cs | 10 +++++----- .../src/Npm/NodeScriptRunner.cs | 12 ++++++------ .../ReactDevelopmentServerMiddleware.cs | 10 +++++----- .../SpaServices.Extensions/src/SpaOptions.cs | 15 +++++++-------- 5 files changed, 27 insertions(+), 28 deletions(-) diff --git a/src/Middleware/SpaServices.Extensions/src/AngularCli/AngularCliBuilder.cs b/src/Middleware/SpaServices.Extensions/src/AngularCli/AngularCliBuilder.cs index b6cc1f18c6..df19fa76d9 100644 --- a/src/Middleware/SpaServices.Extensions/src/AngularCli/AngularCliBuilder.cs +++ b/src/Middleware/SpaServices.Extensions/src/AngularCli/AngularCliBuilder.cs @@ -40,7 +40,7 @@ namespace Microsoft.AspNetCore.SpaServices.AngularCli /// public async Task Build(ISpaBuilder spaBuilder) { - var pkgManagerName = spaBuilder.Options.PackageManagerName; + var pkgManagerCommand = spaBuilder.Options.PackageManagerCommand; var sourcePath = spaBuilder.Options.SourcePath; if (string.IsNullOrEmpty(sourcePath)) { @@ -55,7 +55,7 @@ namespace Microsoft.AspNetCore.SpaServices.AngularCli _scriptName, "--watch", null, - pkgManagerName); + pkgManagerCommand); scriptRunner.AttachToLogger(logger); using (var stdOutReader = new EventedStreamStringReader(scriptRunner.StdOut)) @@ -69,14 +69,14 @@ namespace Microsoft.AspNetCore.SpaServices.AngularCli catch (EndOfStreamException ex) { throw new InvalidOperationException( - $"The {pkgManagerName} script '{_scriptName}' exited without indicating success.\n" + + $"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 {pkgManagerName} script '{_scriptName}' timed out without indicating success. " + + $"The {pkgManagerCommand} script '{_scriptName}' timed out without indicating success. " + $"Output was: {stdOutReader.ReadAsString()}\n" + $"Error output was: {stdErrReader.ReadAsString()}", ex); } diff --git a/src/Middleware/SpaServices.Extensions/src/AngularCli/AngularCliMiddleware.cs b/src/Middleware/SpaServices.Extensions/src/AngularCli/AngularCliMiddleware.cs index dfddd1800a..c4e109b8f7 100644 --- a/src/Middleware/SpaServices.Extensions/src/AngularCli/AngularCliMiddleware.cs +++ b/src/Middleware/SpaServices.Extensions/src/AngularCli/AngularCliMiddleware.cs @@ -25,7 +25,7 @@ namespace Microsoft.AspNetCore.SpaServices.AngularCli ISpaBuilder spaBuilder, string scriptName) { - var pkgManagerName = spaBuilder.Options.PackageManagerName; + var pkgManagerCommand = spaBuilder.Options.PackageManagerCommand; var sourcePath = spaBuilder.Options.SourcePath; if (string.IsNullOrEmpty(sourcePath)) { @@ -40,7 +40,7 @@ namespace Microsoft.AspNetCore.SpaServices.AngularCli // Start Angular CLI and attach to middleware pipeline var appBuilder = spaBuilder.ApplicationBuilder; var logger = LoggerFinder.GetOrCreateLogger(appBuilder, LogCategoryName); - var angularCliServerInfoTask = StartAngularCliServerAsync(sourcePath, scriptName, pkgManagerName, logger); + var angularCliServerInfoTask = StartAngularCliServerAsync(sourcePath, scriptName, pkgManagerCommand, logger); // Everything we proxy is hardcoded to target http://localhost because: // - the requests are always from the local machine (we're not accepting remote @@ -63,13 +63,13 @@ namespace Microsoft.AspNetCore.SpaServices.AngularCli } private static async Task StartAngularCliServerAsync( - string sourcePath, string scriptName, string pkgManagerName, ILogger logger) + string sourcePath, string scriptName, string pkgManagerCommand, ILogger logger) { var portNumber = TcpPortFinder.FindAvailablePort(); logger.LogInformation($"Starting @angular/cli on port {portNumber}..."); var scriptRunner = new NodeScriptRunner( - sourcePath, scriptName, $"--port {portNumber}", null, pkgManagerName); + sourcePath, scriptName, $"--port {portNumber}", null, pkgManagerCommand); scriptRunner.AttachToLogger(logger); Match openBrowserLine; @@ -83,7 +83,7 @@ namespace Microsoft.AspNetCore.SpaServices.AngularCli catch (EndOfStreamException ex) { throw new InvalidOperationException( - $"The {pkgManagerName} script '{scriptName}' exited without indicating that the " + + $"The {pkgManagerCommand} script '{scriptName}' exited without indicating that the " + $"Angular CLI was listening for requests. The error output was: " + $"{stdErrReader.ReadAsString()}", ex); } diff --git a/src/Middleware/SpaServices.Extensions/src/Npm/NodeScriptRunner.cs b/src/Middleware/SpaServices.Extensions/src/Npm/NodeScriptRunner.cs index 86e6497f19..f08abeb19c 100644 --- a/src/Middleware/SpaServices.Extensions/src/Npm/NodeScriptRunner.cs +++ b/src/Middleware/SpaServices.Extensions/src/Npm/NodeScriptRunner.cs @@ -23,7 +23,7 @@ namespace Microsoft.AspNetCore.NodeServices.Npm private static Regex AnsiColorRegex = new Regex("\x001b\\[[0-9;]*m", RegexOptions.None, TimeSpan.FromSeconds(1)); - public NodeScriptRunner(string workingDirectory, string scriptName, string arguments, IDictionary envVars, string pkgManagerName) + public NodeScriptRunner(string workingDirectory, string scriptName, string arguments, IDictionary envVars, string pkgManagerCommand) { if (string.IsNullOrEmpty(workingDirectory)) { @@ -35,12 +35,12 @@ namespace Microsoft.AspNetCore.NodeServices.Npm throw new ArgumentException("Cannot be null or empty.", nameof(scriptName)); } - if (string.IsNullOrEmpty(pkgManagerName)) + if (string.IsNullOrEmpty(pkgManagerCommand)) { - throw new ArgumentException("Cannot be null or empty.", nameof(pkgManagerName)); + throw new ArgumentException("Cannot be null or empty.", nameof(pkgManagerCommand)); } - var exeToRun = pkgManagerName; + var exeToRun = pkgManagerCommand; var completeArguments = $"run {scriptName} -- {arguments ?? string.Empty}"; if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { @@ -48,7 +48,7 @@ namespace Microsoft.AspNetCore.NodeServices.Npm // directly (except with UseShellExecute=true, but that's no good, because // it prevents capturing stdio). So we need to invoke it via "cmd /c". exeToRun = "cmd"; - completeArguments = $"/c {pkgManagerName} {completeArguments}"; + completeArguments = $"/c {pkgManagerCommand} {completeArguments}"; } var processStartInfo = new ProcessStartInfo(exeToRun) @@ -69,7 +69,7 @@ namespace Microsoft.AspNetCore.NodeServices.Npm } } - var process = LaunchNodeProcess(processStartInfo, pkgManagerName); + var process = LaunchNodeProcess(processStartInfo, pkgManagerCommand); StdOut = new EventedStreamReader(process.StandardOutput); StdErr = new EventedStreamReader(process.StandardError); } diff --git a/src/Middleware/SpaServices.Extensions/src/ReactDevelopmentServer/ReactDevelopmentServerMiddleware.cs b/src/Middleware/SpaServices.Extensions/src/ReactDevelopmentServer/ReactDevelopmentServerMiddleware.cs index e7ab1b3dea..6566fef706 100644 --- a/src/Middleware/SpaServices.Extensions/src/ReactDevelopmentServer/ReactDevelopmentServerMiddleware.cs +++ b/src/Middleware/SpaServices.Extensions/src/ReactDevelopmentServer/ReactDevelopmentServerMiddleware.cs @@ -24,7 +24,7 @@ namespace Microsoft.AspNetCore.SpaServices.ReactDevelopmentServer ISpaBuilder spaBuilder, string scriptName) { - var pkgManagerName = spaBuilder.Options.PackageManagerName; + var pkgManagerCommand = spaBuilder.Options.PackageManagerCommand; var sourcePath = spaBuilder.Options.SourcePath; if (string.IsNullOrEmpty(sourcePath)) { @@ -39,7 +39,7 @@ namespace Microsoft.AspNetCore.SpaServices.ReactDevelopmentServer // Start create-react-app and attach to middleware pipeline var appBuilder = spaBuilder.ApplicationBuilder; var logger = LoggerFinder.GetOrCreateLogger(appBuilder, LogCategoryName); - var portTask = StartCreateReactAppServerAsync(sourcePath, scriptName, pkgManagerName, logger); + var portTask = StartCreateReactAppServerAsync(sourcePath, scriptName, pkgManagerCommand, logger); // Everything we proxy is hardcoded to target http://localhost because: // - the requests are always from the local machine (we're not accepting remote @@ -62,7 +62,7 @@ namespace Microsoft.AspNetCore.SpaServices.ReactDevelopmentServer } private static async Task StartCreateReactAppServerAsync( - string sourcePath, string scriptName, string pkgManagerName, ILogger logger) + string sourcePath, string scriptName, string pkgManagerCommand, ILogger logger) { var portNumber = TcpPortFinder.FindAvailablePort(); logger.LogInformation($"Starting create-react-app server on port {portNumber}..."); @@ -73,7 +73,7 @@ namespace Microsoft.AspNetCore.SpaServices.ReactDevelopmentServer { "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, pkgManagerName); + sourcePath, scriptName, null, envVars, pkgManagerCommand); scriptRunner.AttachToLogger(logger); using (var stdErrReader = new EventedStreamStringReader(scriptRunner.StdErr)) @@ -90,7 +90,7 @@ namespace Microsoft.AspNetCore.SpaServices.ReactDevelopmentServer catch (EndOfStreamException ex) { throw new InvalidOperationException( - $"The {pkgManagerName} script '{scriptName}' exited without indicating that the " + + $"The {pkgManagerCommand} script '{scriptName}' exited without indicating that the " + $"create-react-app server was listening for requests. The error output was: " + $"{stdErrReader.ReadAsString()}", ex); } diff --git a/src/Middleware/SpaServices.Extensions/src/SpaOptions.cs b/src/Middleware/SpaServices.Extensions/src/SpaOptions.cs index 5b3d06a94d..59ccc1eda4 100644 --- a/src/Middleware/SpaServices.Extensions/src/SpaOptions.cs +++ b/src/Middleware/SpaServices.Extensions/src/SpaOptions.cs @@ -14,7 +14,7 @@ namespace Microsoft.AspNetCore.SpaServices public class SpaOptions { private PathString _defaultPage = "/index.html"; - private string _defaultPackageManagerName = "npm"; + private string _packageManagerCommand = "npm"; /// /// Constructs a new instance of . @@ -30,7 +30,7 @@ namespace Microsoft.AspNetCore.SpaServices internal SpaOptions(SpaOptions copyFromOptions) { _defaultPage = copyFromOptions.DefaultPage; - _defaultPackageManagerName = copyFromOptions.PackageManagerName; + _packageManagerCommand = copyFromOptions.PackageManagerCommand; DefaultPageStaticFileOptions = copyFromOptions.DefaultPageStaticFileOptions; SourcePath = copyFromOptions.SourcePath; } @@ -74,20 +74,19 @@ namespace Microsoft.AspNetCore.SpaServices /// Gets or sets the name of the package manager executible, (e.g npm, /// yarn) to run the SPA. /// - /// If not set, npm will be assumed as the default package manager - /// executable + /// The default value is 'npm'. /// - public string PackageManagerName + public string PackageManagerCommand { - get => _defaultPackageManagerName; + get => _packageManagerCommand; set { if (string.IsNullOrEmpty(value)) { - throw new ArgumentException($"The value for {nameof(PackageManagerName)} cannot be null or empty."); + throw new ArgumentException($"The value for {nameof(PackageManagerCommand)} cannot be null or empty."); } - _defaultPackageManagerName = value; + _packageManagerCommand = value; } } From 102bcf6739957d772833d20a78f751a63719a4b8 Mon Sep 17 00:00:00 2001 From: Justin Robb Date: Thu, 1 Aug 2019 12:13:37 -0700 Subject: [PATCH 4/5] Reverting breaking changes with npmScript parameter name --- .../src/AngularCli/AngularCliBuilder.cs | 10 +++++----- .../src/AngularCli/AngularCliMiddlewareExtensions.cs | 6 +++--- .../ReactDevelopmentServerMiddlewareExtensions.cs | 6 +++--- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/Middleware/SpaServices.Extensions/src/AngularCli/AngularCliBuilder.cs b/src/Middleware/SpaServices.Extensions/src/AngularCli/AngularCliBuilder.cs index df19fa76d9..c78e194726 100644 --- a/src/Middleware/SpaServices.Extensions/src/AngularCli/AngularCliBuilder.cs +++ b/src/Middleware/SpaServices.Extensions/src/AngularCli/AngularCliBuilder.cs @@ -26,15 +26,15 @@ namespace Microsoft.AspNetCore.SpaServices.AngularCli /// /// Constructs an instance of . /// - /// The name of the script in your package.json file that builds the server-side bundle for your Angular application. - public AngularCliBuilder(string scriptName) + /// The name of the script in your package.json file that builds the server-side bundle for your Angular application. + public AngularCliBuilder(string npmScript) { - if (string.IsNullOrEmpty(scriptName)) + if (string.IsNullOrEmpty(npmScript)) { - throw new ArgumentException("Cannot be null or empty.", nameof(scriptName)); + throw new ArgumentException("Cannot be null or empty.", nameof(npmScript)); } - _scriptName = scriptName; + _scriptName = npmScript; } /// diff --git a/src/Middleware/SpaServices.Extensions/src/AngularCli/AngularCliMiddlewareExtensions.cs b/src/Middleware/SpaServices.Extensions/src/AngularCli/AngularCliMiddlewareExtensions.cs index 59f8ee5046..8f8176447b 100644 --- a/src/Middleware/SpaServices.Extensions/src/AngularCli/AngularCliMiddlewareExtensions.cs +++ b/src/Middleware/SpaServices.Extensions/src/AngularCli/AngularCliMiddlewareExtensions.cs @@ -20,10 +20,10 @@ namespace Microsoft.AspNetCore.SpaServices.AngularCli /// sure not to enable the Angular CLI server. /// /// The . - /// The name of the script in your package.json file that launches the Angular CLI process. + /// The name of the script in your package.json file that launches the Angular CLI process. public static void UseAngularCliServer( this ISpaBuilder spaBuilder, - string scriptName) + string npmScript) { if (spaBuilder == null) { @@ -37,7 +37,7 @@ namespace Microsoft.AspNetCore.SpaServices.AngularCli throw new InvalidOperationException($"To use {nameof(UseAngularCliServer)}, you must supply a non-empty value for the {nameof(SpaOptions.SourcePath)} property of {nameof(SpaOptions)} when calling {nameof(SpaApplicationBuilderExtensions.UseSpa)}."); } - AngularCliMiddleware.Attach(spaBuilder, scriptName); + AngularCliMiddleware.Attach(spaBuilder, npmScript); } } } diff --git a/src/Middleware/SpaServices.Extensions/src/ReactDevelopmentServer/ReactDevelopmentServerMiddlewareExtensions.cs b/src/Middleware/SpaServices.Extensions/src/ReactDevelopmentServer/ReactDevelopmentServerMiddlewareExtensions.cs index 37532329ee..346e839046 100644 --- a/src/Middleware/SpaServices.Extensions/src/ReactDevelopmentServer/ReactDevelopmentServerMiddlewareExtensions.cs +++ b/src/Middleware/SpaServices.Extensions/src/ReactDevelopmentServer/ReactDevelopmentServerMiddlewareExtensions.cs @@ -20,10 +20,10 @@ namespace Microsoft.AspNetCore.SpaServices.ReactDevelopmentServer /// 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. + /// 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 scriptName) + string npmScript) { if (spaBuilder == null) { @@ -37,7 +37,7 @@ namespace Microsoft.AspNetCore.SpaServices.ReactDevelopmentServer 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, scriptName); + ReactDevelopmentServerMiddleware.Attach(spaBuilder, npmScript); } } } From 9adf27409eb742282692764ef4eaaf7018fc022b Mon Sep 17 00:00:00 2001 From: Justin Robb Date: Thu, 1 Aug 2019 13:46:56 -0700 Subject: [PATCH 5/5] Regenerating reference assemblies for Microsoft.AspNetCore.SpaServices.Extensions --- .../Microsoft.AspNetCore.SpaServices.Extensions.netcoreapp3.0.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Middleware/SpaServices.Extensions/ref/Microsoft.AspNetCore.SpaServices.Extensions.netcoreapp3.0.cs b/src/Middleware/SpaServices.Extensions/ref/Microsoft.AspNetCore.SpaServices.Extensions.netcoreapp3.0.cs index be28dc70e2..9dfedd1041 100644 --- a/src/Middleware/SpaServices.Extensions/ref/Microsoft.AspNetCore.SpaServices.Extensions.netcoreapp3.0.cs +++ b/src/Middleware/SpaServices.Extensions/ref/Microsoft.AspNetCore.SpaServices.Extensions.netcoreapp3.0.cs @@ -38,6 +38,7 @@ namespace Microsoft.AspNetCore.SpaServices public SpaOptions() { } public Microsoft.AspNetCore.Http.PathString DefaultPage { get { throw null; } set { } } public Microsoft.AspNetCore.Builder.StaticFileOptions DefaultPageStaticFileOptions { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + public string PackageManagerCommand { get { throw null; } set { } } public string SourcePath { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } public System.TimeSpan StartupTimeout { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } }