From 95cba7f5ddb980bc5919ade7a121eeb011aab15f Mon Sep 17 00:00:00 2001 From: Andrei Tserakhau Date: Mon, 23 May 2016 11:28:42 +0300 Subject: [PATCH] refactor: apply default vs transform to xproj refactor(spa-services): clean code refactor(node-services): clean code, extract classes nto separate files refactor(angular-services): prime cache cleanup --- samples/angular/MusicStore/MusicStore.xproj | 4 +- .../ES2015Transpilation.xproj | 4 +- samples/react/ReactGrid/ReactGrid.xproj | 4 +- ...Microsoft.AspNetCore.AngularServices.xproj | 5 +- .../PrimeCacheHelper.cs | 45 +-- .../Configuration.cs | 46 ++-- .../HostingModels/HttpNodeInstance.cs | 82 ++++-- .../InputOutputStreamNodeInstance.cs | 85 +++--- .../HostingModels/NodeInvocationInfo.cs | 9 +- .../HostingModels/OutOfProcessNodeInstance.cs | 257 ++++++++++-------- .../INodeInstance.cs | 6 +- .../Microsoft.AspNetCore.NodeServices.xproj | 5 +- .../NodeHostingModel.cs | 6 +- .../NodeServicesOptions.cs | 14 + .../Util/EmbeddedResourceReader.cs | 14 +- .../Util/StringAsTempFile.cs | 50 ++-- .../Microsoft.AspNetCore.ReactServices.xproj | 5 +- .../Microsoft.AspNetCore.SpaServices.xproj | 5 +- .../Prerendering/JavaScriptModuleExport.cs | 14 + .../Prerendering/PrerenderTagHelper.cs | 77 +++--- .../Prerendering/Prerenderer.cs | 38 +-- .../Prerendering/RenderToStringResult.cs | 10 + .../Routing/SpaRouteConstraint.cs | 32 +-- .../Routing/SpaRouteExtensions.cs | 46 +++- .../Webpack/ConditionalProxyMiddleware.cs | 98 ++++--- .../ConditionalProxyMiddlewareOptions.cs | 16 ++ .../Webpack/WebpackDevMiddleware.cs | 68 +++-- .../Webpack/WebpackDevMiddlewareOptions.cs | 8 +- templates/Angular2Spa/Angular2Spa.xproj | 4 +- templates/ReactReduxSpa/ReactReduxSpa.xproj | 4 +- templates/ReactSpa/ReactSpa.xproj | 4 +- .../WebApplicationBasic.xproj | 4 +- 32 files changed, 621 insertions(+), 448 deletions(-) create mode 100644 src/Microsoft.AspNetCore.NodeServices/NodeServicesOptions.cs create mode 100644 src/Microsoft.AspNetCore.SpaServices/Prerendering/JavaScriptModuleExport.cs create mode 100644 src/Microsoft.AspNetCore.SpaServices/Prerendering/RenderToStringResult.cs create mode 100644 src/Microsoft.AspNetCore.SpaServices/Webpack/ConditionalProxyMiddlewareOptions.cs diff --git a/samples/angular/MusicStore/MusicStore.xproj b/samples/angular/MusicStore/MusicStore.xproj index 2b74858..93efb5e 100644 --- a/samples/angular/MusicStore/MusicStore.xproj +++ b/samples/angular/MusicStore/MusicStore.xproj @@ -10,11 +10,11 @@ 1a74148f-9dc0-435d-b5ac-7d1b0d3d5e0b MusicStore ..\artifacts\obj\$(MSBuildProjectName) - ..\artifacts\bin\$(MSBuildProjectName)\ + .\bin\ 2.0 5068 - + \ No newline at end of file diff --git a/samples/misc/ES2015Transpilation/ES2015Transpilation.xproj b/samples/misc/ES2015Transpilation/ES2015Transpilation.xproj index 9f87ae0..ba3c063 100644 --- a/samples/misc/ES2015Transpilation/ES2015Transpilation.xproj +++ b/samples/misc/ES2015Transpilation/ES2015Transpilation.xproj @@ -10,11 +10,11 @@ 6d4bcdd6-7951-449b-be55-cb7f014b7430 ES2015Transpilation ..\..\..\JavaScriptServices.sln\artifacts\obj\$(MSBuildProjectName) - ..\..\..\JavaScriptServices.sln\artifacts\bin\$(MSBuildProjectName)\ + .\bin\ 2.0 2018 - + \ No newline at end of file diff --git a/samples/react/ReactGrid/ReactGrid.xproj b/samples/react/ReactGrid/ReactGrid.xproj index 7abf60c..0d002a6 100644 --- a/samples/react/ReactGrid/ReactGrid.xproj +++ b/samples/react/ReactGrid/ReactGrid.xproj @@ -10,11 +10,11 @@ abf90a5b-f4e0-438c-a6e4-9549fb43690b ReactGrid ..\..\..\artifacts\obj\$(MSBuildProjectName) - ..\..\..\artifacts\bin\$(MSBuildProjectName)\ + .\bin\ 2.0 2311 - + \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.AngularServices/Microsoft.AspNetCore.AngularServices.xproj b/src/Microsoft.AspNetCore.AngularServices/Microsoft.AspNetCore.AngularServices.xproj index b037b32..701af9a 100644 --- a/src/Microsoft.AspNetCore.AngularServices/Microsoft.AspNetCore.AngularServices.xproj +++ b/src/Microsoft.AspNetCore.AngularServices/Microsoft.AspNetCore.AngularServices.xproj @@ -9,11 +9,10 @@ 421807e6-b62c-417b-b901-46c5dedaa8f1 Microsoft.AspNetCore.AngularServices ..\artifacts\obj\$(MSBuildProjectName) - ..\artifacts\bin\$(MSBuildProjectName)\ + .\bin\ - 2.0 - + \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.AngularServices/PrimeCacheHelper.cs b/src/Microsoft.AspNetCore.AngularServices/PrimeCacheHelper.cs index 48fdeca..ca9f603 100644 --- a/src/Microsoft.AspNetCore.AngularServices/PrimeCacheHelper.cs +++ b/src/Microsoft.AspNetCore.AngularServices/PrimeCacheHelper.cs @@ -3,42 +3,51 @@ using System.Net; using System.Net.Http; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.AspNetCore.Html; using Microsoft.Extensions.Logging; using Newtonsoft.Json; -namespace Microsoft.AspNetCore.AngularServices { - public static class PrimeCacheHelper { - public static async Task PrimeCache(this IHtmlHelper html, string url) { +namespace Microsoft.AspNetCore.AngularServices +{ + public static class PrimeCacheHelper + { + public static async Task PrimeCache(this IHtmlHelper html, string url) + { // TODO: Consider deduplicating the PrimeCache calls (that is, if there are multiple requests to precache // the same URL, only return nonempty for one of them). This will make it easier to auto-prime-cache any // HTTP requests made during server-side rendering, without risking unnecessary duplicate requests. - if (string.IsNullOrEmpty(url)) { + if (string.IsNullOrEmpty(url)) + { throw new ArgumentException("Value cannot be null or empty", nameof(url)); } - try { + try + { var request = html.ViewContext.HttpContext.Request; - var baseUri = new Uri(string.Concat(request.Scheme, "://", request.Host.ToUriComponent(), request.PathBase.ToUriComponent(), request.Path.ToUriComponent(), request.QueryString.ToUriComponent())); + var baseUri = + new Uri(string.Concat(request.Scheme, "://", request.Host.ToUriComponent(), + request.PathBase.ToUriComponent(), request.Path.ToUriComponent(), + request.QueryString.ToUriComponent())); var fullUri = new Uri(baseUri, url); var response = await new HttpClient().GetAsync(fullUri.ToString()); var responseBody = await response.Content.ReadAsStringAsync(); return new HtmlString(FormatAsScript(url, response.StatusCode, responseBody)); - } catch (Exception ex) { - var logger = (ILogger)html.ViewContext.HttpContext.RequestServices.GetService(typeof (ILogger)); - if (logger != null) { - logger.LogWarning("Error priming cache for URL: " + url, ex); - } + } + catch (Exception ex) + { + var logger = (ILogger)html.ViewContext.HttpContext.RequestServices.GetService(typeof(ILogger)); + logger?.LogWarning("Error priming cache for URL: " + url, ex); return new HtmlString(string.Empty); } } private static string FormatAsScript(string url, HttpStatusCode responseStatusCode, string responseBody) - { - return string.Format(@"", - JsonConvert.SerializeObject(url), - JsonConvert.SerializeObject(new { statusCode = responseStatusCode, body = responseBody }) - ); - } + => + ""; } -} +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.NodeServices/Configuration.cs b/src/Microsoft.AspNetCore.NodeServices/Configuration.cs index 5d85d07..d40733a 100644 --- a/src/Microsoft.AspNetCore.NodeServices/Configuration.cs +++ b/src/Microsoft.AspNetCore.NodeServices/Configuration.cs @@ -2,31 +2,37 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.PlatformAbstractions; using Microsoft.AspNetCore.Hosting; -namespace Microsoft.AspNetCore.NodeServices { - public static class Configuration { - private readonly static string[] defaultWatchFileExtensions = new[] { ".js", ".jsx", ".ts", ".tsx", ".json", ".html" }; - private readonly static NodeServicesOptions defaultOptions = new NodeServicesOptions { +namespace Microsoft.AspNetCore.NodeServices +{ + using System; + + public static class Configuration + { + private static readonly string[] DefaultWatchFileExtensions = {".js", ".jsx", ".ts", ".tsx", ".json", ".html"}; + + private static readonly NodeServicesOptions DefaultOptions = new NodeServicesOptions + { HostingModel = NodeHostingModel.Http, - WatchFileExtensions = defaultWatchFileExtensions + WatchFileExtensions = DefaultWatchFileExtensions }; - public static void AddNodeServices(this IServiceCollection serviceCollection) { - AddNodeServices(serviceCollection, defaultOptions); - } + public static void AddNodeServices(this IServiceCollection serviceCollection) + => AddNodeServices(serviceCollection, DefaultOptions); - public static void AddNodeServices(this IServiceCollection serviceCollection, NodeServicesOptions options) { - serviceCollection.AddSingleton(typeof(INodeServices), (serviceProvider) => { + public static void AddNodeServices(this IServiceCollection serviceCollection, NodeServicesOptions options) + => serviceCollection.AddSingleton(typeof(INodeServices), serviceProvider => + { var hostEnv = serviceProvider.GetRequiredService(); - if (string.IsNullOrEmpty(options.ProjectPath)) { + if (string.IsNullOrEmpty(options.ProjectPath)) + { options.ProjectPath = hostEnv.ContentRootPath; } return CreateNodeServices(options); }); - } public static INodeServices CreateNodeServices(NodeServicesOptions options) { - var watchFileExtensions = options.WatchFileExtensions ?? defaultWatchFileExtensions; + var watchFileExtensions = options.WatchFileExtensions ?? DefaultWatchFileExtensions; switch (options.HostingModel) { case NodeHostingModel.Http: @@ -34,18 +40,8 @@ namespace Microsoft.AspNetCore.NodeServices { case NodeHostingModel.InputOutputStream: return new InputOutputStreamNodeInstance(options.ProjectPath); default: - throw new System.ArgumentException("Unknown hosting model: " + options.HostingModel.ToString()); + throw new ArgumentException("Unknown hosting model: " + options.HostingModel); } } } - - public class NodeServicesOptions { - public NodeHostingModel HostingModel { get; set; } - public string ProjectPath { get; set; } - public string[] WatchFileExtensions { get; set; } - - public NodeServicesOptions() { - this.HostingModel = NodeHostingModel.Http; - } - } -} +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.NodeServices/HostingModels/HttpNodeInstance.cs b/src/Microsoft.AspNetCore.NodeServices/HostingModels/HttpNodeInstance.cs index 4671740..8cff883 100644 --- a/src/Microsoft.AspNetCore.NodeServices/HostingModels/HttpNodeInstance.cs +++ b/src/Microsoft.AspNetCore.NodeServices/HostingModels/HttpNodeInstance.cs @@ -7,66 +7,90 @@ using System.Threading.Tasks; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; -namespace Microsoft.AspNetCore.NodeServices { - internal class HttpNodeInstance : OutOfProcessNodeInstance { - private readonly static Regex PortMessageRegex = new Regex(@"^\[Microsoft.AspNetCore.NodeServices.HttpNodeHost:Listening on port (\d+)\]$"); +namespace Microsoft.AspNetCore.NodeServices +{ + internal class HttpNodeInstance : OutOfProcessNodeInstance + { + private static readonly Regex PortMessageRegex = + new Regex(@"^\[Microsoft.AspNetCore.NodeServices.HttpNodeHost:Listening on port (\d+)\]$"); - private readonly static JsonSerializerSettings jsonSerializerSettings = new JsonSerializerSettings { + private static readonly JsonSerializerSettings JsonSerializerSettings = new JsonSerializerSettings + { ContractResolver = new CamelCasePropertyNamesContractResolver() }; private int _portNumber; - public HttpNodeInstance(string projectPath, int port = 0, string[] watchFileExtensions = null) - : base(EmbeddedResourceReader.Read(typeof(HttpNodeInstance), "/Content/Node/entrypoint-http.js"), projectPath, MakeCommandLineOptions(port, watchFileExtensions)) + public HttpNodeInstance(string projectPath, int port = 0, string[] watchFileExtensions = null) + : base( + EmbeddedResourceReader.Read( + typeof(HttpNodeInstance), + "/Content/Node/entrypoint-http.js"), + projectPath, + MakeCommandLineOptions(port, watchFileExtensions)) { - } + } - private static string MakeCommandLineOptions(int port, string[] watchFileExtensions) { - var result = "--port " + port.ToString(); - if (watchFileExtensions != null && watchFileExtensions.Length > 0) { + private static string MakeCommandLineOptions(int port, string[] watchFileExtensions) + { + var result = "--port " + port; + if (watchFileExtensions != null && watchFileExtensions.Length > 0) + { result += " --watch " + string.Join(",", watchFileExtensions); } + return result; } - public override async Task Invoke(NodeInvocationInfo invocationInfo) { - await this.EnsureReady(); + public override async Task Invoke(NodeInvocationInfo invocationInfo) + { + await EnsureReady(); - using (var client = new HttpClient()) { + using (var client = new HttpClient()) + { // TODO: Use System.Net.Http.Formatting (PostAsJsonAsync etc.) - var payloadJson = JsonConvert.SerializeObject(invocationInfo, jsonSerializerSettings); + var payloadJson = JsonConvert.SerializeObject(invocationInfo, JsonSerializerSettings); var payload = new StringContent(payloadJson, Encoding.UTF8, "application/json"); - var response = await client.PostAsync("http://localhost:" + this._portNumber, payload); + var response = await client.PostAsync("http://localhost:" + _portNumber, payload); var responseString = await response.Content.ReadAsStringAsync(); - if (!response.IsSuccessStatusCode) { + if (!response.IsSuccessStatusCode) + { throw new Exception("Call to Node module failed with error: " + responseString); } var responseIsJson = response.Content.Headers.ContentType.MediaType == "application/json"; - if (responseIsJson) { + if (responseIsJson) + { return JsonConvert.DeserializeObject(responseString); - } else if (typeof(T) != typeof(string)) { - throw new System.ArgumentException("Node module responded with non-JSON string. This cannot be converted to the requested generic type: " + typeof(T).FullName); - } else { - return (T)(object)responseString; } + if (typeof(T) != typeof(string)) + { + throw new ArgumentException( + "Node module responded with non-JSON string. This cannot be converted to the requested generic type: " + + typeof(T).FullName); + } + return (T)(object)responseString; } } - protected override void OnOutputDataReceived(string outputData) { - var match = this._portNumber != 0 ? null : PortMessageRegex.Match(outputData); - if (match != null && match.Success) { - this._portNumber = int.Parse(match.Groups[1].Captures[0].Value); - } else { + protected override void OnOutputDataReceived(string outputData) + { + var match = _portNumber != 0 ? null : PortMessageRegex.Match(outputData); + if (match != null && match.Success) + { + _portNumber = int.Parse(match.Groups[1].Captures[0].Value); + } + else + { base.OnOutputDataReceived(outputData); } } - protected override void OnBeforeLaunchProcess() { + protected override void OnBeforeLaunchProcess() + { // Prepare to receive a new port number - this._portNumber = 0; + _portNumber = 0; } } -} +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.NodeServices/HostingModels/InputOutputStreamNodeInstance.cs b/src/Microsoft.AspNetCore.NodeServices/HostingModels/InputOutputStreamNodeInstance.cs index 3f27acd..ffa2cdc 100644 --- a/src/Microsoft.AspNetCore.NodeServices/HostingModels/InputOutputStreamNodeInstance.cs +++ b/src/Microsoft.AspNetCore.NodeServices/HostingModels/InputOutputStreamNodeInstance.cs @@ -4,55 +4,72 @@ using System.Threading.Tasks; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; -namespace Microsoft.AspNetCore.NodeServices { - // This is just to demonstrate that other transports are possible. This implementation is extremely - // dubious - if the Node-side code fails to conform to the expected protocol in any way (e.g., has an - // error), then it will just hang forever. So don't use this. - // - // But it's fast - the communication round-trip time is about 0.2ms (tested on OS X on a recent machine), - // versus 2-3ms for the HTTP transport. - // - // Instead of directly using stdin/stdout, we could use either regular sockets (TCP) or use named pipes - // on Windows and domain sockets on Linux / OS X, but either way would need a system for framing the - // requests, associating them with responses, and scheduling use of the comms channel. +namespace Microsoft.AspNetCore.NodeServices +{ + /// + /// This is just to demonstrate that other transports are possible. This implementation is extremely + /// dubious - if the Node-side code fails to conform to the expected protocol in any way (e.g., has an + /// error), then it will just hang forever. So don't use this. + /// + /// But it's fast - the communication round-trip time is about 0.2ms (tested on OS X on a recent machine), + /// versus 2-3ms for the HTTP transport. + /// + /// Instead of directly using stdin/stdout, we could use either regular sockets (TCP) or use named pipes + /// on Windows and domain sockets on Linux / OS X, but either way would need a system for framing the + /// requests, associating them with responses, and scheduling use of the comms channel. + /// + /// internal class InputOutputStreamNodeInstance : OutOfProcessNodeInstance { - private SemaphoreSlim _invocationSemaphore = new SemaphoreSlim(1); - private TaskCompletionSource _currentInvocationResult; - - private readonly static JsonSerializerSettings jsonSerializerSettings = new JsonSerializerSettings { + private static readonly JsonSerializerSettings JsonSerializerSettings = new JsonSerializerSettings + { ContractResolver = new CamelCasePropertyNamesContractResolver() }; - public InputOutputStreamNodeInstance(string projectPath) - : base(EmbeddedResourceReader.Read(typeof(InputOutputStreamNodeInstance), "/Content/Node/entrypoint-stream.js"), projectPath) + private TaskCompletionSource _currentInvocationResult; + private readonly SemaphoreSlim _invocationSemaphore = new SemaphoreSlim(1); + + public InputOutputStreamNodeInstance(string projectPath) + : base( + EmbeddedResourceReader.Read( + typeof(InputOutputStreamNodeInstance), + "/Content/Node/entrypoint-stream.js"), + projectPath) { - } + } - public override async Task Invoke(NodeInvocationInfo invocationInfo) { - await this._invocationSemaphore.WaitAsync(); - try { - await this.EnsureReady(); + public override async Task Invoke(NodeInvocationInfo invocationInfo) + { + await _invocationSemaphore.WaitAsync(); + try + { + await EnsureReady(); - var payloadJson = JsonConvert.SerializeObject(invocationInfo, jsonSerializerSettings); - var nodeProcess = this.NodeProcess; - this._currentInvocationResult = new TaskCompletionSource(); + var payloadJson = JsonConvert.SerializeObject(invocationInfo, JsonSerializerSettings); + var nodeProcess = NodeProcess; + _currentInvocationResult = new TaskCompletionSource(); nodeProcess.StandardInput.Write("\ninvoke:"); nodeProcess.StandardInput.WriteLine(payloadJson); // WriteLineAsync isn't supported cross-platform - var resultString = await this._currentInvocationResult.Task; + var resultString = await _currentInvocationResult.Task; return JsonConvert.DeserializeObject(resultString); - } finally { - this._invocationSemaphore.Release(); - this._currentInvocationResult = null; + } + finally + { + _invocationSemaphore.Release(); + _currentInvocationResult = null; } } - protected override void OnOutputDataReceived(string outputData) { - if (this._currentInvocationResult != null) { - this._currentInvocationResult.SetResult(outputData); - } else { + protected override void OnOutputDataReceived(string outputData) + { + if (_currentInvocationResult != null) + { + _currentInvocationResult.SetResult(outputData); + } + else + { base.OnOutputDataReceived(outputData); } } } -} +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.NodeServices/HostingModels/NodeInvocationInfo.cs b/src/Microsoft.AspNetCore.NodeServices/HostingModels/NodeInvocationInfo.cs index a4fde64..2e196f6 100644 --- a/src/Microsoft.AspNetCore.NodeServices/HostingModels/NodeInvocationInfo.cs +++ b/src/Microsoft.AspNetCore.NodeServices/HostingModels/NodeInvocationInfo.cs @@ -1,8 +1,9 @@ -namespace Microsoft.AspNetCore.NodeServices { +namespace Microsoft.AspNetCore.NodeServices +{ public class NodeInvocationInfo { - public string ModuleName; - public string ExportedFunctionName; - public object[] Args; + public string ModuleName { get; set; } + public string ExportedFunctionName { get; set; } + public object[] Args { get; set; } } } diff --git a/src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs b/src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs index fe3386a..65e21a3 100644 --- a/src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs +++ b/src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs @@ -3,126 +3,42 @@ using System.Diagnostics; using System.IO; using System.Threading.Tasks; -namespace Microsoft.AspNetCore.NodeServices { - /** - * Class responsible for launching the Node child process, determining when it is ready to accept invocations, - * and finally killing it when the parent process exits. Also it restarts the child process if it dies. - */ - public abstract class OutOfProcessNodeInstance : INodeServices { - private object _childProcessLauncherLock; - private bool disposed; - private StringAsTempFile _entryPointScript; - private string _projectPath; - private string _commandLineArguments; - private Process _nodeProcess; +namespace Microsoft.AspNetCore.NodeServices +{ + /// + /// Class responsible for launching the Node child process, determining when it is ready to accept invocations, + /// and finally killing it when the parent process exits. Also it restarts the child process if it dies. + /// + /// + public abstract class OutOfProcessNodeInstance : INodeServices + { + private readonly object _childProcessLauncherLock; + private readonly string _commandLineArguments; + private readonly StringAsTempFile _entryPointScript; private TaskCompletionSource _nodeProcessIsReadySource; - - protected Process NodeProcess { - get { - // This is only exposed to support the unreliable OutOfProcessNodeRunner, which is just to verify that - // other hosting/transport mechanisms are possible. This shouldn't really be exposed. - return this._nodeProcess; - } - } + private readonly string _projectPath; + private bool _disposed; public OutOfProcessNodeInstance(string entryPointScript, string projectPath, string commandLineArguments = null) { - this._childProcessLauncherLock = new object(); - this._entryPointScript = new StringAsTempFile(entryPointScript); - this._projectPath = projectPath; - this._commandLineArguments = commandLineArguments ?? string.Empty; + _childProcessLauncherLock = new object(); + _entryPointScript = new StringAsTempFile(entryPointScript); + _projectPath = projectPath; + _commandLineArguments = commandLineArguments ?? string.Empty; } - public abstract Task Invoke(NodeInvocationInfo invocationInfo); + protected Process NodeProcess { get; private set; } - public Task Invoke(string moduleName, params object[] args) { - return this.InvokeExport(moduleName, null, args); - } + public Task Invoke(string moduleName, params object[] args) + => InvokeExport(moduleName, null, args); - public async Task InvokeExport(string moduleName, string exportedFunctionName, params object[] args) { - return await this.Invoke(new NodeInvocationInfo { + public Task InvokeExport(string moduleName, string exportedFunctionName, params object[] args) + => Invoke(new NodeInvocationInfo + { ModuleName = moduleName, ExportedFunctionName = exportedFunctionName, Args = args }); - } - - protected async Task EnsureReady() { - lock (this._childProcessLauncherLock) { - if (this._nodeProcess == null || this._nodeProcess.HasExited) { - var startInfo = new ProcessStartInfo("node") { - Arguments = "\"" + this._entryPointScript.FileName + "\" " + this._commandLineArguments, - UseShellExecute = false, - RedirectStandardInput = true, - RedirectStandardOutput = true, - RedirectStandardError = true, - WorkingDirectory = this._projectPath - }; - - // Append projectPath to NODE_PATH so it can locate node_modules - var existingNodePath = Environment.GetEnvironmentVariable("NODE_PATH") ?? string.Empty; - if (existingNodePath != string.Empty) { - existingNodePath += ":"; - } - - var nodePathValue = existingNodePath + Path.Combine(this._projectPath, "node_modules"); - #if NET451 - startInfo.EnvironmentVariables.Add("NODE_PATH", nodePathValue); - #else - startInfo.Environment.Add("NODE_PATH", nodePathValue); - #endif - - this.OnBeforeLaunchProcess(); - this._nodeProcess = Process.Start(startInfo); - this.ConnectToInputOutputStreams(); - } - } - - var task = this._nodeProcessIsReadySource.Task; - var initializationSucceeded = await task; - - if (!initializationSucceeded) { - throw new InvalidOperationException("The Node.js process failed to initialize", task.Exception); - } - } - - private void ConnectToInputOutputStreams() { - var initializationIsCompleted = false; // TODO: Make this thread-safe? (Interlocked.Exchange etc.) - this._nodeProcessIsReadySource = new TaskCompletionSource(); - - this._nodeProcess.OutputDataReceived += (sender, evt) => { - if (evt.Data == "[Microsoft.AspNetCore.NodeServices:Listening]" && !initializationIsCompleted) { - this._nodeProcessIsReadySource.SetResult(true); - initializationIsCompleted = true; - } else if (evt.Data != null) { - this.OnOutputDataReceived(evt.Data); - } - }; - - this._nodeProcess.ErrorDataReceived += (sender, evt) => { - if (evt.Data != null) { - this.OnErrorDataReceived(evt.Data); - if (!initializationIsCompleted) { - this._nodeProcessIsReadySource.SetResult(false); - initializationIsCompleted = true; - } - } - }; - - this._nodeProcess.BeginOutputReadLine(); - this._nodeProcess.BeginErrorReadLine(); - } - - protected virtual void OnBeforeLaunchProcess() { - } - - protected virtual void OnOutputDataReceived(string outputData) { - Console.WriteLine("[Node] " + outputData); - } - - protected virtual void OnErrorDataReceived(string errorData) { - Console.WriteLine("[Node] " + errorData); - } public void Dispose() { @@ -130,23 +46,124 @@ namespace Microsoft.AspNetCore.NodeServices { GC.SuppressFinalize(this); } - protected virtual void Dispose(bool disposing) + public abstract Task Invoke(NodeInvocationInfo invocationInfo); + + protected async Task EnsureReady() { - if (!disposed) { - if (disposing) { - this._entryPointScript.Dispose(); - } + lock (_childProcessLauncherLock) + { + if (NodeProcess == null || NodeProcess.HasExited) + { + var startInfo = new ProcessStartInfo("node") + { + Arguments = "\"" + _entryPointScript.FileName + "\" " + _commandLineArguments, + UseShellExecute = false, + RedirectStandardInput = true, + RedirectStandardOutput = true, + RedirectStandardError = true, + WorkingDirectory = _projectPath + }; - if (this._nodeProcess != null && !this._nodeProcess.HasExited) { - this._nodeProcess.Kill(); // TODO: Is there a more graceful way to end it? Or does this still let it perform any cleanup? - } + // Append projectPath to NODE_PATH so it can locate node_modules + var existingNodePath = Environment.GetEnvironmentVariable("NODE_PATH") ?? string.Empty; + if (existingNodePath != string.Empty) + { + existingNodePath += ":"; + } - disposed = true; + var nodePathValue = existingNodePath + Path.Combine(_projectPath, "node_modules"); +#if NET451 + startInfo.EnvironmentVariables.Add("NODE_PATH", nodePathValue); +#else + startInfo.Environment.Add("NODE_PATH", nodePathValue); +#endif + + OnBeforeLaunchProcess(); + NodeProcess = Process.Start(startInfo); + ConnectToInputOutputStreams(); + } + } + + var task = _nodeProcessIsReadySource.Task; + var initializationSucceeded = await task; + + if (!initializationSucceeded) + { + throw new InvalidOperationException("The Node.js process failed to initialize", task.Exception); } } - ~OutOfProcessNodeInstance() { - Dispose (false); + private void ConnectToInputOutputStreams() + { + var initializationIsCompleted = false; // TODO: Make this thread-safe? (Interlocked.Exchange etc.) + _nodeProcessIsReadySource = new TaskCompletionSource(); + + NodeProcess.OutputDataReceived += (sender, evt) => + { + if (evt.Data == "[Microsoft.AspNetCore.NodeServices:Listening]" && !initializationIsCompleted) + { + _nodeProcessIsReadySource.SetResult(true); + initializationIsCompleted = true; + } + else if (evt.Data != null) + { + OnOutputDataReceived(evt.Data); + } + }; + + NodeProcess.ErrorDataReceived += (sender, evt) => + { + if (evt.Data != null) + { + OnErrorDataReceived(evt.Data); + if (!initializationIsCompleted) + { + _nodeProcessIsReadySource.SetResult(false); + initializationIsCompleted = true; + } + } + }; + + NodeProcess.BeginOutputReadLine(); + NodeProcess.BeginErrorReadLine(); + } + + protected virtual void OnBeforeLaunchProcess() + { + } + + protected virtual void OnOutputDataReceived(string outputData) + { + Console.WriteLine("[Node] " + outputData); + } + + protected virtual void OnErrorDataReceived(string errorData) + { + Console.WriteLine("[Node] " + errorData); + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + _entryPointScript.Dispose(); + } + + if (NodeProcess != null && !NodeProcess.HasExited) + { + NodeProcess.Kill(); + // TODO: Is there a more graceful way to end it? Or does this still let it perform any cleanup? + } + + _disposed = true; + } + } + + ~OutOfProcessNodeInstance() + { + Dispose(false); } } -} +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.NodeServices/INodeInstance.cs b/src/Microsoft.AspNetCore.NodeServices/INodeInstance.cs index fae73db..0c17ea1 100644 --- a/src/Microsoft.AspNetCore.NodeServices/INodeInstance.cs +++ b/src/Microsoft.AspNetCore.NodeServices/INodeInstance.cs @@ -1,8 +1,10 @@ using System; using System.Threading.Tasks; -namespace Microsoft.AspNetCore.NodeServices { - public interface INodeServices : IDisposable { +namespace Microsoft.AspNetCore.NodeServices +{ + public interface INodeServices : IDisposable + { Task Invoke(string moduleName, params object[] args); Task InvokeExport(string moduleName, string exportedFunctionName, params object[] args); diff --git a/src/Microsoft.AspNetCore.NodeServices/Microsoft.AspNetCore.NodeServices.xproj b/src/Microsoft.AspNetCore.NodeServices/Microsoft.AspNetCore.NodeServices.xproj index f03c112..ee1f038 100644 --- a/src/Microsoft.AspNetCore.NodeServices/Microsoft.AspNetCore.NodeServices.xproj +++ b/src/Microsoft.AspNetCore.NodeServices/Microsoft.AspNetCore.NodeServices.xproj @@ -9,11 +9,10 @@ b0fa4175-8b29-4904-9780-28b3c24b0567 Microsoft.AspNetCore.NodeServices ..\JavaScriptServices.sln\artifacts\obj\$(MSBuildProjectName) - ..\JavaScriptServices.sln\artifacts\bin\$(MSBuildProjectName)\ + .\bin\ - 2.0 - + \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.NodeServices/NodeHostingModel.cs b/src/Microsoft.AspNetCore.NodeServices/NodeHostingModel.cs index b363631..64d9170 100644 --- a/src/Microsoft.AspNetCore.NodeServices/NodeHostingModel.cs +++ b/src/Microsoft.AspNetCore.NodeServices/NodeHostingModel.cs @@ -1,5 +1,7 @@ -namespace Microsoft.AspNetCore.NodeServices { - public enum NodeHostingModel { +namespace Microsoft.AspNetCore.NodeServices +{ + public enum NodeHostingModel + { Http, InputOutputStream, } diff --git a/src/Microsoft.AspNetCore.NodeServices/NodeServicesOptions.cs b/src/Microsoft.AspNetCore.NodeServices/NodeServicesOptions.cs new file mode 100644 index 0000000..7452c5f --- /dev/null +++ b/src/Microsoft.AspNetCore.NodeServices/NodeServicesOptions.cs @@ -0,0 +1,14 @@ +namespace Microsoft.AspNetCore.NodeServices +{ + public class NodeServicesOptions + { + public NodeServicesOptions() + { + HostingModel = NodeHostingModel.Http; + } + + public NodeHostingModel HostingModel { get; set; } + public string ProjectPath { get; set; } + public string[] WatchFileExtensions { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.NodeServices/Util/EmbeddedResourceReader.cs b/src/Microsoft.AspNetCore.NodeServices/Util/EmbeddedResourceReader.cs index 73fe29e..a7147c1 100644 --- a/src/Microsoft.AspNetCore.NodeServices/Util/EmbeddedResourceReader.cs +++ b/src/Microsoft.AspNetCore.NodeServices/Util/EmbeddedResourceReader.cs @@ -2,16 +2,20 @@ using System; using System.IO; using System.Reflection; -namespace Microsoft.AspNetCore.NodeServices { - public static class EmbeddedResourceReader { - public static string Read(Type assemblyContainingType, string path) { +namespace Microsoft.AspNetCore.NodeServices +{ + public static class EmbeddedResourceReader + { + public static string Read(Type assemblyContainingType, string path) + { var asm = assemblyContainingType.GetTypeInfo().Assembly; var embeddedResourceName = asm.GetName().Name + path.Replace("/", "."); using (var stream = asm.GetManifestResourceStream(embeddedResourceName)) - using (var sr = new StreamReader(stream)) { + using (var sr = new StreamReader(stream)) + { return sr.ReadToEnd(); } } } -} +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.NodeServices/Util/StringAsTempFile.cs b/src/Microsoft.AspNetCore.NodeServices/Util/StringAsTempFile.cs index bfcb333..a9f04a9 100644 --- a/src/Microsoft.AspNetCore.NodeServices/Util/StringAsTempFile.cs +++ b/src/Microsoft.AspNetCore.NodeServices/Util/StringAsTempFile.cs @@ -1,39 +1,45 @@ using System; using System.IO; -namespace Microsoft.AspNetCore.NodeServices { +namespace Microsoft.AspNetCore.NodeServices +{ // Makes it easier to pass script files to Node in a way that's sure to clean up after the process exits - public sealed class StringAsTempFile : IDisposable { - public string FileName { get; private set; } - + public sealed class StringAsTempFile : IDisposable + { private bool _disposedValue; - public StringAsTempFile(string content) { - this.FileName = Path.GetTempFileName(); - File.WriteAllText(this.FileName, content); - } - - private void DisposeImpl(bool disposing) + public StringAsTempFile(string content) { - if (!_disposedValue) { - if (disposing) { - // TODO: dispose managed state (managed objects). - } - - File.Delete(this.FileName); - - _disposedValue = true; - } + FileName = Path.GetTempFileName(); + File.WriteAllText(FileName, content); } + public string FileName { get; } + public void Dispose() { DisposeImpl(true); GC.SuppressFinalize(this); } - ~StringAsTempFile() { - DisposeImpl(false); + private void DisposeImpl(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + // TODO: dispose managed state (managed objects). + } + + File.Delete(FileName); + + _disposedValue = true; + } + } + + ~StringAsTempFile() + { + DisposeImpl(false); } } -} +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.ReactServices/Microsoft.AspNetCore.ReactServices.xproj b/src/Microsoft.AspNetCore.ReactServices/Microsoft.AspNetCore.ReactServices.xproj index 0f54276..ae962e2 100644 --- a/src/Microsoft.AspNetCore.ReactServices/Microsoft.AspNetCore.ReactServices.xproj +++ b/src/Microsoft.AspNetCore.ReactServices/Microsoft.AspNetCore.ReactServices.xproj @@ -9,11 +9,10 @@ b04381de-991f-4831-a0b5-fe1bd3ef80c4 Microsoft.AspNetCore.ReactServices ..\artifacts\obj\$(MSBuildProjectName) - ..\artifacts\bin\$(MSBuildProjectName)\ + .\bin\ - 2.0 - + \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.SpaServices/Microsoft.AspNetCore.SpaServices.xproj b/src/Microsoft.AspNetCore.SpaServices/Microsoft.AspNetCore.SpaServices.xproj index 99a66ba..31f1d76 100644 --- a/src/Microsoft.AspNetCore.SpaServices/Microsoft.AspNetCore.SpaServices.xproj +++ b/src/Microsoft.AspNetCore.SpaServices/Microsoft.AspNetCore.SpaServices.xproj @@ -9,11 +9,10 @@ 4624f728-6dff-44b6-93b5-3c7d9c94bf3f Microsoft.AspNetCore.SpaServices ..\artifacts\obj\$(MSBuildProjectName) - ..\artifacts\bin\$(MSBuildProjectName)\ + .\bin\ - 2.0 - + \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.SpaServices/Prerendering/JavaScriptModuleExport.cs b/src/Microsoft.AspNetCore.SpaServices/Prerendering/JavaScriptModuleExport.cs new file mode 100644 index 0000000..054a693 --- /dev/null +++ b/src/Microsoft.AspNetCore.SpaServices/Prerendering/JavaScriptModuleExport.cs @@ -0,0 +1,14 @@ +namespace Microsoft.AspNetCore.SpaServices.Prerendering +{ + public class JavaScriptModuleExport + { + public JavaScriptModuleExport(string moduleName) + { + this.ModuleName = moduleName; + } + + public string ModuleName { get; private set; } + public string ExportName { get; set; } + public string WebpackConfig { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.SpaServices/Prerendering/PrerenderTagHelper.cs b/src/Microsoft.AspNetCore.SpaServices/Prerendering/PrerenderTagHelper.cs index ee07d6f..ea95821 100644 --- a/src/Microsoft.AspNetCore.SpaServices/Prerendering/PrerenderTagHelper.cs +++ b/src/Microsoft.AspNetCore.SpaServices/Prerendering/PrerenderTagHelper.cs @@ -16,11 +16,31 @@ namespace Microsoft.AspNetCore.SpaServices.Prerendering [HtmlTargetElement(Attributes = PrerenderModuleAttributeName)] public class PrerenderTagHelper : TagHelper { - static INodeServices fallbackNodeServices; // Used only if no INodeServices was registered with DI + private const string PrerenderModuleAttributeName = "asp-prerender-module"; + private const string PrerenderExportAttributeName = "asp-prerender-export"; + private const string PrerenderWebpackConfigAttributeName = "asp-prerender-webpack-config"; + private static INodeServices _fallbackNodeServices; // Used only if no INodeServices was registered with DI - const string PrerenderModuleAttributeName = "asp-prerender-module"; - const string PrerenderExportAttributeName = "asp-prerender-export"; - const string PrerenderWebpackConfigAttributeName = "asp-prerender-webpack-config"; + private readonly string _applicationBasePath; + private readonly INodeServices _nodeServices; + + public PrerenderTagHelper(IServiceProvider serviceProvider) + { + var hostEnv = (IHostingEnvironment) serviceProvider.GetService(typeof(IHostingEnvironment)); + _nodeServices = (INodeServices) serviceProvider.GetService(typeof(INodeServices)) ?? _fallbackNodeServices; + _applicationBasePath = hostEnv.ContentRootPath; + + // Consider removing the following. Having it means you can get away with not putting app.AddNodeServices() + // in your startup file, but then again it might be confusing that you don't need to. + if (_nodeServices == null) + { + _nodeServices = _fallbackNodeServices = Configuration.CreateNodeServices(new NodeServicesOptions + { + HostingModel = NodeHostingModel.Http, + ProjectPath = _applicationBasePath + }); + } + } [HtmlAttributeName(PrerenderModuleAttributeName)] public string ModuleName { get; set; } @@ -35,52 +55,37 @@ namespace Microsoft.AspNetCore.SpaServices.Prerendering [ViewContext] public ViewContext ViewContext { get; set; } - private string applicationBasePath; - private INodeServices nodeServices; - - public PrerenderTagHelper(IServiceProvider serviceProvider) - { - var hostEnv = (IHostingEnvironment)serviceProvider.GetService(typeof (IHostingEnvironment)); - this.nodeServices = (INodeServices)serviceProvider.GetService(typeof (INodeServices)) ?? fallbackNodeServices; - this.applicationBasePath = hostEnv.ContentRootPath; - - // Consider removing the following. Having it means you can get away with not putting app.AddNodeServices() - // in your startup file, but then again it might be confusing that you don't need to. - if (this.nodeServices == null) { - this.nodeServices = fallbackNodeServices = Configuration.CreateNodeServices(new NodeServicesOptions { - HostingModel = NodeHostingModel.Http, - ProjectPath = this.applicationBasePath - }); - } - } - public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output) { - var request = this.ViewContext.HttpContext.Request; + var request = ViewContext.HttpContext.Request; var result = await Prerenderer.RenderToString( - applicationBasePath: this.applicationBasePath, - nodeServices: this.nodeServices, - bootModule: new JavaScriptModuleExport(this.ModuleName) { - exportName = this.ExportName, - webpackConfig = this.WebpackConfigPath + _applicationBasePath, + _nodeServices, + new JavaScriptModuleExport(ModuleName) + { + ExportName = ExportName, + WebpackConfig = WebpackConfigPath }, - requestAbsoluteUrl: UriHelper.GetEncodedUrl(request), - requestPathAndQuery: request.Path + request.QueryString.Value); + request.GetEncodedUrl(), + request.Path + request.QueryString.Value); output.Content.SetHtmlContent(result.Html); // Also attach any specified globals to the 'window' object. This is useful for transferring // general state between server and client. - if (result.Globals != null) { + if (result.Globals != null) + { var stringBuilder = new StringBuilder(); - foreach (var property in result.Globals.Properties()) { + foreach (var property in result.Globals.Properties()) + { stringBuilder.AppendFormat("window.{0} = {1};", property.Name, property.Value.ToString(Formatting.None)); } - if (stringBuilder.Length > 0) { - output.PostElement.SetHtmlContent($""); + if (stringBuilder.Length > 0) + { + output.PostElement.SetHtmlContent($""); } } } } -} +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.SpaServices/Prerendering/Prerenderer.cs b/src/Microsoft.AspNetCore.SpaServices/Prerendering/Prerenderer.cs index 287d547..5ed8552 100644 --- a/src/Microsoft.AspNetCore.SpaServices/Prerendering/Prerenderer.cs +++ b/src/Microsoft.AspNetCore.SpaServices/Prerendering/Prerenderer.cs @@ -1,42 +1,34 @@ using System; using System.Threading.Tasks; using Microsoft.AspNetCore.NodeServices; -using Newtonsoft.Json.Linq; namespace Microsoft.AspNetCore.SpaServices.Prerendering { public static class Prerenderer { - private static Lazy nodeScript; + private static readonly Lazy NodeScript; - static Prerenderer() { - nodeScript = new Lazy(() => { + static Prerenderer() + { + NodeScript = new Lazy(() => + { var script = EmbeddedResourceReader.Read(typeof(Prerenderer), "/Content/Node/prerenderer.js"); return new StringAsTempFile(script); // Will be cleaned up on process exit }); } - public static async Task RenderToString(string applicationBasePath, INodeServices nodeServices, JavaScriptModuleExport bootModule, string requestAbsoluteUrl, string requestPathAndQuery) { - return await nodeServices.InvokeExport(nodeScript.Value.FileName, "renderToString", + public static async Task RenderToString( + string applicationBasePath, + INodeServices nodeServices, + JavaScriptModuleExport bootModule, + string requestAbsoluteUrl, + string requestPathAndQuery) + => await nodeServices.InvokeExport( + NodeScript.Value.FileName, + "renderToString", applicationBasePath, bootModule, requestAbsoluteUrl, requestPathAndQuery); - } } - - public class JavaScriptModuleExport { - public string moduleName { get; private set; } - public string exportName { get; set; } - public string webpackConfig { get; set; } - - public JavaScriptModuleExport(string moduleName) { - this.moduleName = moduleName; - } - } - - public class RenderToStringResult { - public string Html; - public JObject Globals; - } -} +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.SpaServices/Prerendering/RenderToStringResult.cs b/src/Microsoft.AspNetCore.SpaServices/Prerendering/RenderToStringResult.cs new file mode 100644 index 0000000..1d5b482 --- /dev/null +++ b/src/Microsoft.AspNetCore.SpaServices/Prerendering/RenderToStringResult.cs @@ -0,0 +1,10 @@ +using Newtonsoft.Json.Linq; + +namespace Microsoft.AspNetCore.SpaServices.Prerendering +{ + public class RenderToStringResult + { + public JObject Globals { get; set; } + public string Html { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.SpaServices/Routing/SpaRouteConstraint.cs b/src/Microsoft.AspNetCore.SpaServices/Routing/SpaRouteConstraint.cs index 00243d6..025d11a 100644 --- a/src/Microsoft.AspNetCore.SpaServices/Routing/SpaRouteConstraint.cs +++ b/src/Microsoft.AspNetCore.SpaServices/Routing/SpaRouteConstraint.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; @@ -7,26 +6,27 @@ namespace Microsoft.AspNetCore.SpaServices { internal class SpaRouteConstraint : IRouteConstraint { - private readonly string clientRouteTokenName; + private readonly string _clientRouteTokenName; - public SpaRouteConstraint(string clientRouteTokenName) { - if (string.IsNullOrEmpty(clientRouteTokenName)) { - throw new ArgumentException("Value cannot be null or empty", "clientRouteTokenName"); + public SpaRouteConstraint(string clientRouteTokenName) + { + if (string.IsNullOrEmpty(clientRouteTokenName)) + { + throw new ArgumentException("Value cannot be null or empty", nameof(clientRouteTokenName)); } - this.clientRouteTokenName = clientRouteTokenName; + _clientRouteTokenName = clientRouteTokenName; } - public bool Match(HttpContext httpContext, IRouter route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection) - { - var clientRouteValue = (values[this.clientRouteTokenName] as string) ?? string.Empty; - return !HasDotInLastSegment(clientRouteValue); - } + public bool Match( + HttpContext httpContext, + IRouter route, + string routeKey, + RouteValueDictionary values, + RouteDirection routeDirection) + => !HasDotInLastSegment(values[_clientRouteTokenName] as string ?? string.Empty); private bool HasDotInLastSegment(string uri) - { - var lastSegmentStartPos = uri.LastIndexOf('/'); - return uri.IndexOf('.', lastSegmentStartPos + 1) >= 0; - } + => uri.IndexOf('.', uri.LastIndexOf('/') + 1) >= 0; } -} +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.SpaServices/Routing/SpaRouteExtensions.cs b/src/Microsoft.AspNetCore.SpaServices/Routing/SpaRouteExtensions.cs index 192c839..7bdebc6 100644 --- a/src/Microsoft.AspNetCore.SpaServices/Routing/SpaRouteExtensions.cs +++ b/src/Microsoft.AspNetCore.SpaServices/Routing/SpaRouteExtensions.cs @@ -4,18 +4,34 @@ using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.SpaServices; // Putting in this namespace so it's always available whenever MapRoute is + namespace Microsoft.AspNetCore.Builder { public static class SpaRouteExtensions { private const string ClientRouteTokenName = "clientRoute"; - public static void MapSpaFallbackRoute(this IRouteBuilder routeBuilder, string name, object defaults, object constraints = null, object dataTokens = null) - { - MapSpaFallbackRoute(routeBuilder, name, /* templatePrefix */ (string)null, defaults, constraints, dataTokens); - } + public static void MapSpaFallbackRoute( + this IRouteBuilder routeBuilder, + string name, + object defaults, + object constraints = null, + object dataTokens = null) + => MapSpaFallbackRoute( + routeBuilder, + name, + /* templatePrefix */ null, + defaults, + constraints, + dataTokens); - public static void MapSpaFallbackRoute(this IRouteBuilder routeBuilder, string name, string templatePrefix, object defaults, object constraints = null, object dataTokens = null) + public static void MapSpaFallbackRoute( + this IRouteBuilder routeBuilder, + string name, + string templatePrefix, + object defaults, + object constraints = null, + object dataTokens = null) { var template = CreateRouteTemplate(templatePrefix); @@ -29,25 +45,27 @@ namespace Microsoft.AspNetCore.Builder { templatePrefix = templatePrefix ?? string.Empty; - if (templatePrefix.Contains("?")) { + if (templatePrefix.Contains("?")) + { // TODO: Consider supporting this. The {*clientRoute} part should be added immediately before the '?' throw new ArgumentException("SPA fallback route templates don't support querystrings"); } - if (templatePrefix.Contains("#")) { - throw new ArgumentException("SPA fallback route templates should not include # characters. The hash part of a URI does not get sent to the server."); + if (templatePrefix.Contains("#")) + { + throw new ArgumentException( + "SPA fallback route templates should not include # characters. The hash part of a URI does not get sent to the server."); } - if (templatePrefix != string.Empty && !templatePrefix.EndsWith("/")) { + if (templatePrefix != string.Empty && !templatePrefix.EndsWith("/")) + { templatePrefix += "/"; } - return templatePrefix + $"{{*{ ClientRouteTokenName }}}"; + return templatePrefix + $"{{*{ClientRouteTokenName}}}"; } private static IDictionary ObjectToDictionary(object value) - { - return value as IDictionary ?? new RouteValueDictionary(value); - } + => value as IDictionary ?? new RouteValueDictionary(value); } -} +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.SpaServices/Webpack/ConditionalProxyMiddleware.cs b/src/Microsoft.AspNetCore.SpaServices/Webpack/ConditionalProxyMiddleware.cs index 8c82a8f..63c4f3e 100644 --- a/src/Microsoft.AspNetCore.SpaServices/Webpack/ConditionalProxyMiddleware.cs +++ b/src/Microsoft.AspNetCore.SpaServices/Webpack/ConditionalProxyMiddleware.cs @@ -8,65 +8,85 @@ using Microsoft.AspNetCore.Http; namespace Microsoft.AspNetCore.SpaServices.Webpack { - // Based on https://github.com/aspnet/Proxy/blob/dev/src/Microsoft.AspNetCore.Proxy/ProxyMiddleware.cs - // Differs in that, if the proxied request returns a 404, we pass through to the next middleware in the chain - // This is useful for Webpack middleware, because it lets you fall back on prebuilt files on disk for - // chunks not exposed by the current Webpack config (e.g., DLL/vendor chunks). - internal class ConditionalProxyMiddleware { - private RequestDelegate next; - private ConditionalProxyMiddlewareOptions options; - private HttpClient httpClient; - private string pathPrefix; + /// + /// Based on https://github.com/aspnet/Proxy/blob/dev/src/Microsoft.AspNetCore.Proxy/ProxyMiddleware.cs + /// Differs in that, if the proxied request returns a 404, we pass through to the next middleware in the chain + /// This is useful for Webpack middleware, because it lets you fall back on prebuilt files on disk for + /// chunks not exposed by the current Webpack config (e.g., DLL/vendor chunks). + /// + internal class ConditionalProxyMiddleware + { + private readonly HttpClient _httpClient; + private readonly RequestDelegate _next; + private readonly ConditionalProxyMiddlewareOptions _options; + private readonly string _pathPrefix; - public ConditionalProxyMiddleware(RequestDelegate next, string pathPrefix, ConditionalProxyMiddlewareOptions options) + public ConditionalProxyMiddleware( + RequestDelegate next, + string pathPrefix, + ConditionalProxyMiddlewareOptions options) { - this.next = next; - this.pathPrefix = pathPrefix; - this.options = options; - this.httpClient = new HttpClient(new HttpClientHandler()); + _next = next; + _pathPrefix = pathPrefix; + _options = options; + _httpClient = new HttpClient(new HttpClientHandler()); } - + public async Task Invoke(HttpContext context) { - if (context.Request.Path.StartsWithSegments(this.pathPrefix)) { + if (context.Request.Path.StartsWithSegments(_pathPrefix)) + { var didProxyRequest = await PerformProxyRequest(context); - if (didProxyRequest) { + if (didProxyRequest) + { return; } } - + // Not a request we can proxy - await this.next.Invoke(context); + await _next.Invoke(context); } - - private async Task PerformProxyRequest(HttpContext context) { + + private async Task PerformProxyRequest(HttpContext context) + { var requestMessage = new HttpRequestMessage(); // Copy the request headers - foreach (var header in context.Request.Headers) { - if (!requestMessage.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray()) && requestMessage.Content != null) { + foreach (var header in context.Request.Headers) + { + if (!requestMessage.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray())) + { requestMessage.Content?.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray()); } } - - requestMessage.Headers.Host = options.Host + ":" + options.Port; - var uriString = $"{options.Scheme}://{options.Host}:{options.Port}{context.Request.PathBase}{context.Request.Path}{context.Request.QueryString}"; + + requestMessage.Headers.Host = _options.Host + ":" + _options.Port; + var uriString = + $"{_options.Scheme}://{_options.Host}:{_options.Port}{context.Request.PathBase}{context.Request.Path}{context.Request.QueryString}"; requestMessage.RequestUri = new Uri(uriString); requestMessage.Method = new HttpMethod(context.Request.Method); - using (var responseMessage = await httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, context.RequestAborted)) { - if (responseMessage.StatusCode == HttpStatusCode.NotFound) { + using ( + var responseMessage = + await + _httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, + context.RequestAborted)) + { + if (responseMessage.StatusCode == HttpStatusCode.NotFound) + { // Let some other middleware handle this return false; } - + // We can handle this - context.Response.StatusCode = (int)responseMessage.StatusCode; - foreach (var header in responseMessage.Headers) { + context.Response.StatusCode = (int) responseMessage.StatusCode; + foreach (var header in responseMessage.Headers) + { context.Response.Headers[header.Key] = header.Value.ToArray(); } - foreach (var header in responseMessage.Content.Headers) { + foreach (var header in responseMessage.Content.Headers) + { context.Response.Headers[header.Key] = header.Value.ToArray(); } @@ -77,16 +97,4 @@ namespace Microsoft.AspNetCore.SpaServices.Webpack } } } - - internal class ConditionalProxyMiddlewareOptions { - public string Scheme { get; private set; } - public string Host { get; private set; } - public string Port { get; private set; } - - public ConditionalProxyMiddlewareOptions(string scheme, string host, string port) { - this.Scheme = scheme; - this.Host = host; - this.Port = port; - } - } -} +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.SpaServices/Webpack/ConditionalProxyMiddlewareOptions.cs b/src/Microsoft.AspNetCore.SpaServices/Webpack/ConditionalProxyMiddlewareOptions.cs new file mode 100644 index 0000000..5654007 --- /dev/null +++ b/src/Microsoft.AspNetCore.SpaServices/Webpack/ConditionalProxyMiddlewareOptions.cs @@ -0,0 +1,16 @@ +namespace Microsoft.AspNetCore.SpaServices.Webpack +{ + internal class ConditionalProxyMiddlewareOptions + { + public ConditionalProxyMiddlewareOptions(string scheme, string host, string port) + { + Scheme = scheme; + Host = host; + Port = port; + } + + public string Scheme { get; } + public string Host { get; } + public string Port { get; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.SpaServices/Webpack/WebpackDevMiddleware.cs b/src/Microsoft.AspNetCore.SpaServices/Webpack/WebpackDevMiddleware.cs index dd8a188..f998c55 100644 --- a/src/Microsoft.AspNetCore.SpaServices/Webpack/WebpackDevMiddleware.cs +++ b/src/Microsoft.AspNetCore.SpaServices/Webpack/WebpackDevMiddleware.cs @@ -9,22 +9,31 @@ using Microsoft.Extensions.PlatformAbstractions; using Newtonsoft.Json; // Putting in this namespace so it's always available whenever MapRoute is + namespace Microsoft.AspNetCore.Builder { public static class WebpackDevMiddleware { - const string WebpackDevMiddlewareScheme = "http"; - const string WebpackDevMiddlewareHostname = "localhost"; - const string WebpackHotMiddlewareEndpoint = "/__webpack_hmr"; - const string DefaultConfigFile = "webpack.config.js"; + private const string WebpackDevMiddlewareScheme = "http"; + private const string WebpackDevMiddlewareHostname = "localhost"; + private const string WebpackHotMiddlewareEndpoint = "/__webpack_hmr"; + private const string DefaultConfigFile = "webpack.config.js"; - public static void UseWebpackDevMiddleware(this IApplicationBuilder appBuilder, WebpackDevMiddlewareOptions options = null) { - // Validate options - if (options == null) { + public static void UseWebpackDevMiddleware( + this IApplicationBuilder appBuilder, + WebpackDevMiddlewareOptions options = null) + { + // Prepare options + if (options == null) + { options = new WebpackDevMiddlewareOptions(); } - if (options.ReactHotModuleReplacement && !options.HotModuleReplacement) { - throw new ArgumentException("To enable ReactHotModuleReplacement, you must also enable HotModuleReplacement."); + + // Validate options + if (options.ReactHotModuleReplacement && !options.HotModuleReplacement) + { + throw new ArgumentException( + "To enable ReactHotModuleReplacement, you must also enable HotModuleReplacement."); } // Unlike other consumers of NodeServices, WebpackDevMiddleware dosen't share Node instances, nor does it @@ -32,44 +41,55 @@ namespace Microsoft.AspNetCore.Builder // because it must *not* restart when files change (if it did, you'd lose all the benefits of Webpack // middleware). And since this is a dev-time-only feature, it doesn't matter if the default transport isn't // as fast as some theoretical future alternative. - var hostEnv = (IHostingEnvironment)appBuilder.ApplicationServices.GetService(typeof (IHostingEnvironment)); - var nodeServices = Configuration.CreateNodeServices(new NodeServicesOptions { + var hostEnv = (IHostingEnvironment)appBuilder.ApplicationServices.GetService(typeof(IHostingEnvironment)); + var nodeServices = Configuration.CreateNodeServices(new NodeServicesOptions + { HostingModel = NodeHostingModel.Http, ProjectPath = hostEnv.ContentRootPath, - WatchFileExtensions = new string[] {} // Don't watch anything + WatchFileExtensions = new string[] { } // Don't watch anything }); // Get a filename matching the middleware Node script - var script = EmbeddedResourceReader.Read(typeof (WebpackDevMiddleware), "/Content/Node/webpack-dev-middleware.js"); + var script = EmbeddedResourceReader.Read(typeof(WebpackDevMiddleware), + "/Content/Node/webpack-dev-middleware.js"); var nodeScript = new StringAsTempFile(script); // Will be cleaned up on process exit // Tell Node to start the server hosting webpack-dev-middleware - var devServerOptions = new { + var devServerOptions = new + { webpackConfigPath = Path.Combine(hostEnv.ContentRootPath, options.ConfigFile ?? DefaultConfigFile), suppliedOptions = options }; - var devServerInfo = nodeServices.InvokeExport(nodeScript.FileName, "createWebpackDevServer", JsonConvert.SerializeObject(devServerOptions)).Result; + var devServerInfo = + nodeServices.InvokeExport(nodeScript.FileName, "createWebpackDevServer", + JsonConvert.SerializeObject(devServerOptions)).Result; // Proxy the corresponding requests through ASP.NET and into the Node listener - var proxyOptions = new ConditionalProxyMiddlewareOptions(WebpackDevMiddlewareScheme, WebpackDevMiddlewareHostname, devServerInfo.Port.ToString()); + var proxyOptions = new ConditionalProxyMiddlewareOptions(WebpackDevMiddlewareScheme, + WebpackDevMiddlewareHostname, devServerInfo.Port.ToString()); appBuilder.UseMiddleware(devServerInfo.PublicPath, proxyOptions); // While it would be nice to proxy the /__webpack_hmr requests too, these return an EventStream, // and the Microsoft.AspNetCore.Proxy code doesn't handle that entirely - it throws an exception after // a while. So, just serve a 302 for those. - appBuilder.Map(WebpackHotMiddlewareEndpoint, builder => { - builder.Use(next => async ctx => { - ctx.Response.Redirect($"{ WebpackDevMiddlewareScheme }://{ WebpackDevMiddlewareHostname }:{ devServerInfo.Port.ToString() }{ WebpackHotMiddlewareEndpoint }"); + appBuilder.Map(WebpackHotMiddlewareEndpoint, builder => + { + builder.Use(next => async ctx => + { + ctx.Response.Redirect( + $"{WebpackDevMiddlewareScheme}://{WebpackDevMiddlewareHostname}:{devServerInfo.Port.ToString()}{WebpackHotMiddlewareEndpoint}"); await Task.Yield(); }); }); } - #pragma warning disable CS0649 - class WebpackDevServerInfo { - public int Port; - public string PublicPath; +#pragma warning disable CS0649 + class WebpackDevServerInfo + { + public int Port { get; set; } + public string PublicPath { get; set; } } - #pragma warning restore CS0649 } +#pragma warning restore CS0649 } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.SpaServices/Webpack/WebpackDevMiddlewareOptions.cs b/src/Microsoft.AspNetCore.SpaServices/Webpack/WebpackDevMiddlewareOptions.cs index 020589a..08a6dbd 100644 --- a/src/Microsoft.AspNetCore.SpaServices/Webpack/WebpackDevMiddlewareOptions.cs +++ b/src/Microsoft.AspNetCore.SpaServices/Webpack/WebpackDevMiddlewareOptions.cs @@ -1,7 +1,9 @@ -namespace Microsoft.AspNetCore.SpaServices.Webpack { - public class WebpackDevMiddlewareOptions { +namespace Microsoft.AspNetCore.SpaServices.Webpack +{ + public class WebpackDevMiddlewareOptions + { public bool HotModuleReplacement { get; set; } public bool ReactHotModuleReplacement { get; set; } public string ConfigFile { get; set; } } -} +} \ No newline at end of file diff --git a/templates/Angular2Spa/Angular2Spa.xproj b/templates/Angular2Spa/Angular2Spa.xproj index 455712b..d4c5f07 100644 --- a/templates/Angular2Spa/Angular2Spa.xproj +++ b/templates/Angular2Spa/Angular2Spa.xproj @@ -10,11 +10,11 @@ 8f5cb8a9-3086-4b49-a1c2-32a9f89bca11 Angular2Spa ..\..\..\JavaScriptServices.sln\artifacts\obj\$(MSBuildProjectName) - ..\..\..\JavaScriptServices.sln\artifacts\bin\$(MSBuildProjectName)\ + .\bin\ 2.0 2018 - + \ No newline at end of file diff --git a/templates/ReactReduxSpa/ReactReduxSpa.xproj b/templates/ReactReduxSpa/ReactReduxSpa.xproj index 630731a..0996bb6 100644 --- a/templates/ReactReduxSpa/ReactReduxSpa.xproj +++ b/templates/ReactReduxSpa/ReactReduxSpa.xproj @@ -10,11 +10,11 @@ dbfc6db0-a6d1-4694-a108-1c604b988da3 ReactReduxSpa ..\..\..\JavaScriptServices.sln\artifacts\obj\$(MSBuildProjectName) - ..\..\..\JavaScriptServices.sln\artifacts\bin\$(MSBuildProjectName)\ + .\bin\ 2.0 2018 - + \ No newline at end of file diff --git a/templates/ReactSpa/ReactSpa.xproj b/templates/ReactSpa/ReactSpa.xproj index d1b8262..abe5a58 100644 --- a/templates/ReactSpa/ReactSpa.xproj +++ b/templates/ReactSpa/ReactSpa.xproj @@ -10,11 +10,11 @@ e9d1a695-f0e6-46f2-b5e3-72f4af805387 ReactSpa ..\..\..\JavaScriptServices.sln\artifacts\obj\$(MSBuildProjectName) - ..\..\..\JavaScriptServices.sln\artifacts\bin\$(MSBuildProjectName)\ + .\bin\ 2.0 2018 - + \ No newline at end of file diff --git a/templates/WebApplicationBasic/WebApplicationBasic.xproj b/templates/WebApplicationBasic/WebApplicationBasic.xproj index 3845bf2..b0c23a0 100644 --- a/templates/WebApplicationBasic/WebApplicationBasic.xproj +++ b/templates/WebApplicationBasic/WebApplicationBasic.xproj @@ -10,11 +10,11 @@ cb4398d6-b7f1-449a-ae02-828769679232 WebApplicationBasic ..\..\..\JavaScriptServices.sln\artifacts\obj\$(MSBuildProjectName) - ..\..\..\JavaScriptServices.sln\artifacts\bin\$(MSBuildProjectName)\ + .\bin\ 2.0 2018 - + \ No newline at end of file