From 0d0d25b032f9f5939f9361e83f8a17fd3cc4c9fb Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Mon, 15 Aug 2016 14:40:38 -0700 Subject: [PATCH 01/55] In WebpackDevMiddleware, allow configuration of ProjectPath (implements #262) --- .../Webpack/WebpackDevMiddleware.cs | 16 +++++++++++++--- .../Webpack/WebpackDevMiddlewareOptions.cs | 1 + 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/Microsoft.AspNetCore.SpaServices/Webpack/WebpackDevMiddleware.cs b/src/Microsoft.AspNetCore.SpaServices/Webpack/WebpackDevMiddleware.cs index a9398d7..89d6950 100644 --- a/src/Microsoft.AspNetCore.SpaServices/Webpack/WebpackDevMiddleware.cs +++ b/src/Microsoft.AspNetCore.SpaServices/Webpack/WebpackDevMiddleware.cs @@ -35,15 +35,25 @@ namespace Microsoft.AspNetCore.Builder "To enable ReactHotModuleReplacement, you must also enable HotModuleReplacement."); } + string projectPath; + if (options.ProjectPath == null) + { + var hostEnv = (IHostingEnvironment)appBuilder.ApplicationServices.GetService(typeof(IHostingEnvironment)); + projectPath = hostEnv.ContentRootPath; + } + else + { + projectPath = options.ProjectPath; + } + // Unlike other consumers of NodeServices, WebpackDevMiddleware dosen't share Node instances, nor does it // use your DI configuration. It's important for WebpackDevMiddleware to have its own private Node instance // 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 { - ProjectPath = hostEnv.ContentRootPath, + ProjectPath = projectPath, WatchFileExtensions = new string[] { } // Don't watch anything }); @@ -55,7 +65,7 @@ namespace Microsoft.AspNetCore.Builder // Tell Node to start the server hosting webpack-dev-middleware var devServerOptions = new { - webpackConfigPath = Path.Combine(hostEnv.ContentRootPath, options.ConfigFile ?? DefaultConfigFile), + webpackConfigPath = Path.Combine(projectPath, options.ConfigFile ?? DefaultConfigFile), suppliedOptions = options }; var devServerInfo = diff --git a/src/Microsoft.AspNetCore.SpaServices/Webpack/WebpackDevMiddlewareOptions.cs b/src/Microsoft.AspNetCore.SpaServices/Webpack/WebpackDevMiddlewareOptions.cs index fcdde44..ebfe36d 100644 --- a/src/Microsoft.AspNetCore.SpaServices/Webpack/WebpackDevMiddlewareOptions.cs +++ b/src/Microsoft.AspNetCore.SpaServices/Webpack/WebpackDevMiddlewareOptions.cs @@ -6,5 +6,6 @@ namespace Microsoft.AspNetCore.SpaServices.Webpack public int HotModuleReplacementServerPort { get; set; } public bool ReactHotModuleReplacement { get; set; } public string ConfigFile { get; set; } + public string ProjectPath { get; set; } } } \ No newline at end of file From c53bd8f8f613dd90b57eac5db7303589e8635132 Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Tue, 16 Aug 2016 11:44:55 -0700 Subject: [PATCH 02/55] Prerenderer now passes original (unescaped) URL to Node - fixes #250 --- .../Prerendering/PrerenderTagHelper.cs | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/Microsoft.AspNetCore.SpaServices/Prerendering/PrerenderTagHelper.cs b/src/Microsoft.AspNetCore.SpaServices/Prerendering/PrerenderTagHelper.cs index e0dcd29..e773bbb 100644 --- a/src/Microsoft.AspNetCore.SpaServices/Prerendering/PrerenderTagHelper.cs +++ b/src/Microsoft.AspNetCore.SpaServices/Prerendering/PrerenderTagHelper.cs @@ -2,13 +2,11 @@ using System; using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Extensions; +using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.NodeServices; using Microsoft.AspNetCore.Razor.TagHelpers; -using Microsoft.Extensions.PlatformAbstractions; using Newtonsoft.Json; namespace Microsoft.AspNetCore.SpaServices.Prerendering @@ -60,7 +58,19 @@ namespace Microsoft.AspNetCore.SpaServices.Prerendering public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output) { + // We want to pass the original, unencoded incoming URL data through to Node, so that + // server-side code has the same view of the URL as client-side code (on the client, + // location.pathname returns an unencoded string). + // The following logic handles special characters in URL paths in the same way that + // Node and client-side JS does. For example, the path "/a=b%20c" gets passed through + // unchanged (whereas other .NET APIs do change it - Path.Value will return it as + // "/a=b c" and Path.ToString() will return it as "/a%3db%20c") + var requestFeature = ViewContext.HttpContext.Features.Get(); + var unencodedPathAndQuery = requestFeature.RawTarget; + var request = ViewContext.HttpContext.Request; + var unencodedAbsoluteUrl = $"{request.Scheme}://{request.Host}{unencodedPathAndQuery}"; + var result = await Prerenderer.RenderToString( _applicationBasePath, _nodeServices, @@ -69,8 +79,8 @@ namespace Microsoft.AspNetCore.SpaServices.Prerendering ExportName = ExportName, WebpackConfig = WebpackConfigPath }, - request.GetEncodedUrl(), - request.Path + request.QueryString.Value, + unencodedAbsoluteUrl, + unencodedPathAndQuery, CustomDataParameter); output.Content.SetHtmlContent(result.Html); From 56cb898bdeac5fb897b92e38ad708e6d01e79543 Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Tue, 16 Aug 2016 13:57:37 -0700 Subject: [PATCH 03/55] Rename PrimeCache to PrimeCacheAsync (keeping older name as obsolete overload). Fixes #246. --- samples/angular/MusicStore/Views/Home/Index.cshtml | 4 ++-- .../PrimeCacheHelper.cs | 10 ++++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/samples/angular/MusicStore/Views/Home/Index.cshtml b/samples/angular/MusicStore/Views/Home/Index.cshtml index 9ac9f01..a2799f7 100755 --- a/samples/angular/MusicStore/Views/Home/Index.cshtml +++ b/samples/angular/MusicStore/Views/Home/Index.cshtml @@ -4,8 +4,8 @@ Loading... - @await Html.PrimeCache(Url.Action("GenreMenuList", "GenresApi")) - @await Html.PrimeCache(Url.Action("MostPopular", "AlbumsApi")) + @await Html.PrimeCacheAsync(Url.Action("GenreMenuList", "GenresApi")) + @await Html.PrimeCacheAsync(Url.Action("MostPopular", "AlbumsApi")) @section scripts { diff --git a/src/Microsoft.AspNetCore.AngularServices/PrimeCacheHelper.cs b/src/Microsoft.AspNetCore.AngularServices/PrimeCacheHelper.cs index 1b01662..388ab06 100644 --- a/src/Microsoft.AspNetCore.AngularServices/PrimeCacheHelper.cs +++ b/src/Microsoft.AspNetCore.AngularServices/PrimeCacheHelper.cs @@ -11,9 +11,15 @@ namespace Microsoft.AspNetCore.AngularServices { public static class PrimeCacheHelper { - public static async Task PrimeCache(this IHtmlHelper html, string url) + [Obsolete("Use PrimeCacheAsync instead")] + public static Task PrimeCache(this IHtmlHelper html, string url) { - // TODO: Consider deduplicating the PrimeCache calls (that is, if there are multiple requests to precache + return PrimeCacheAsync(html, url); + } + + public static async Task PrimeCacheAsync(this IHtmlHelper html, string url) + { + // TODO: Consider deduplicating the PrimeCacheAsync 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. From 098159998d3d56aa7f255a423558e3bc75cee134 Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Tue, 16 Aug 2016 16:26:07 -0700 Subject: [PATCH 04/55] Add ability to configure environment variables for Node instances, plus auto-populate NODE_ENV based on IHostingEnvironment when possible. Fixes #230 --- .../Configuration/Configuration.cs | 9 +++-- .../Configuration/NodeServicesOptions.cs | 18 ++++++++++ .../HostingModels/HttpNodeInstance.cs | 4 ++- .../HostingModels/OutOfProcessNodeInstance.cs | 34 +++++++++++++++---- .../HostingModels/SocketNodeInstance.cs | 5 ++- .../Prerendering/PrerenderTagHelper.cs | 2 +- .../Webpack/WebpackDevMiddleware.cs | 14 ++------ 7 files changed, 62 insertions(+), 24 deletions(-) diff --git a/src/Microsoft.AspNetCore.NodeServices/Configuration/Configuration.cs b/src/Microsoft.AspNetCore.NodeServices/Configuration/Configuration.cs index 6ecd43f..bd76dc8 100644 --- a/src/Microsoft.AspNetCore.NodeServices/Configuration/Configuration.cs +++ b/src/Microsoft.AspNetCore.NodeServices/Configuration/Configuration.cs @@ -20,12 +20,15 @@ namespace Microsoft.AspNetCore.NodeServices { // Since this instance is being created through DI, we can access the IHostingEnvironment // to populate options.ProjectPath if it wasn't explicitly specified. + var hostEnv = serviceProvider.GetRequiredService(); if (string.IsNullOrEmpty(options.ProjectPath)) { - var hostEnv = serviceProvider.GetRequiredService(); options.ProjectPath = hostEnv.ContentRootPath; } + // Similarly, we can determine the 'is development' value from the hosting environment + options.AddDefaultEnvironmentVariables(hostEnv.IsDevelopment()); + // Likewise, if no logger was specified explicitly, we should use the one from DI. // If it doesn't provide one, CreateNodeInstance will set up a default. if (options.NodeInstanceOutputLogger == null) @@ -69,11 +72,11 @@ namespace Microsoft.AspNetCore.NodeServices { case NodeHostingModel.Http: return new HttpNodeInstance(options.ProjectPath, options.WatchFileExtensions, logger, - options.LaunchWithDebugging, options.DebuggingPort, /* port */ 0); + options.EnvironmentVariables, options.LaunchWithDebugging, options.DebuggingPort, /* port */ 0); case NodeHostingModel.Socket: var pipeName = "pni-" + Guid.NewGuid().ToString("D"); // Arbitrary non-clashing string return new SocketNodeInstance(options.ProjectPath, options.WatchFileExtensions, pipeName, logger, - options.LaunchWithDebugging, options.DebuggingPort); + options.EnvironmentVariables, options.LaunchWithDebugging, options.DebuggingPort); default: throw new ArgumentException("Unknown hosting model: " + options.HostingModel); } diff --git a/src/Microsoft.AspNetCore.NodeServices/Configuration/NodeServicesOptions.cs b/src/Microsoft.AspNetCore.NodeServices/Configuration/NodeServicesOptions.cs index 5e6f518..4ff981e 100644 --- a/src/Microsoft.AspNetCore.NodeServices/Configuration/NodeServicesOptions.cs +++ b/src/Microsoft.AspNetCore.NodeServices/Configuration/NodeServicesOptions.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using Microsoft.AspNetCore.NodeServices.HostingModels; using Microsoft.Extensions.Logging; @@ -22,6 +23,23 @@ namespace Microsoft.AspNetCore.NodeServices public string[] WatchFileExtensions { get; set; } public ILogger NodeInstanceOutputLogger { get; set; } public bool LaunchWithDebugging { get; set; } + public IDictionary EnvironmentVariables { get; set; } public int? DebuggingPort { get; set; } + + public NodeServicesOptions AddDefaultEnvironmentVariables(bool isDevelopmentMode) + { + if (EnvironmentVariables == null) + { + EnvironmentVariables = new Dictionary(); + } + + if (!EnvironmentVariables.ContainsKey("NODE_ENV")) + { + // These strings are a de-facto standard in Node + EnvironmentVariables["NODE_ENV"] = isDevelopmentMode ? "development" : "production"; + } + + return this; + } } } \ 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 0e828ea..9400d6e 100644 --- a/src/Microsoft.AspNetCore.NodeServices/HostingModels/HttpNodeInstance.cs +++ b/src/Microsoft.AspNetCore.NodeServices/HostingModels/HttpNodeInstance.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using System.Net.Http; using System.Text; @@ -34,7 +35,7 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels private int _portNumber; public HttpNodeInstance(string projectPath, string[] watchFileExtensions, ILogger nodeInstanceOutputLogger, - bool launchWithDebugging, int? debuggingPort, int port = 0) + IDictionary environmentVars, bool launchWithDebugging, int? debuggingPort, int port = 0) : base( EmbeddedResourceReader.Read( typeof(HttpNodeInstance), @@ -43,6 +44,7 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels watchFileExtensions, MakeCommandLineOptions(port), nodeInstanceOutputLogger, + environmentVars, launchWithDebugging, debuggingPort) { diff --git a/src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs b/src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs index 3c9cfd5..d9ede58 100644 --- a/src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs +++ b/src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; @@ -46,6 +47,7 @@ If you haven't yet installed node-inspector, you can do so as follows: string[] watchFileExtensions, string commandLineArguments, ILogger nodeOutputLogger, + IDictionary environmentVars, bool launchWithDebugging, int? debuggingPort) { @@ -58,7 +60,7 @@ If you haven't yet installed node-inspector, you can do so as follows: _entryPointScript = new StringAsTempFile(entryPointScript); var startInfo = PrepareNodeProcessStartInfo(_entryPointScript.FileName, projectPath, commandLineArguments, - launchWithDebugging, debuggingPort); + environmentVars, launchWithDebugging, debuggingPort); _nodeProcess = LaunchNodeProcess(startInfo); _watchFileExtensions = watchFileExtensions; _fileSystemWatcher = BeginFileWatcher(projectPath); @@ -99,7 +101,7 @@ If you haven't yet installed node-inspector, you can do so as follows: // This method is virtual, as it provides a way to override the NODE_PATH or the path to node.exe protected virtual ProcessStartInfo PrepareNodeProcessStartInfo( string entryPointFilename, string projectPath, string commandLineArguments, - bool launchWithDebugging, int? debuggingPort) + IDictionary environmentVars, bool launchWithDebugging, int? debuggingPort) { string debuggingArgs; if (launchWithDebugging) @@ -122,6 +124,19 @@ If you haven't yet installed node-inspector, you can do so as follows: WorkingDirectory = projectPath }; + // Append environment vars + if (environmentVars != null) + { + foreach (var envVarKey in environmentVars.Keys) + { + var envVarValue = environmentVars[envVarKey]; + if (envVarValue != null) + { + SetEnvironmentVariable(startInfo, envVarKey, envVarValue); + } + } + } + // Append projectPath to NODE_PATH so it can locate node_modules var existingNodePath = Environment.GetEnvironmentVariable("NODE_PATH") ?? string.Empty; if (existingNodePath != string.Empty) @@ -130,11 +145,7 @@ If you haven't yet installed node-inspector, you can do so as follows: } var nodePathValue = existingNodePath + Path.Combine(projectPath, "node_modules"); -#if NET451 - startInfo.EnvironmentVariables["NODE_PATH"] = nodePathValue; -#else - startInfo.Environment["NODE_PATH"] = nodePathValue; -#endif + SetEnvironmentVariable(startInfo, "NODE_PATH", nodePathValue); return startInfo; } @@ -179,6 +190,15 @@ If you haven't yet installed node-inspector, you can do so as follows: } } + private static void SetEnvironmentVariable(ProcessStartInfo startInfo, string name, string value) + { +#if NET451 + startInfo.EnvironmentVariables[name] = value; +#else + startInfo.Environment[name] = value; +#endif + } + private static Process LaunchNodeProcess(ProcessStartInfo startInfo) { var process = Process.Start(startInfo); diff --git a/src/Microsoft.AspNetCore.NodeServices/HostingModels/SocketNodeInstance.cs b/src/Microsoft.AspNetCore.NodeServices/HostingModels/SocketNodeInstance.cs index 5f3a6ec..46115f6 100644 --- a/src/Microsoft.AspNetCore.NodeServices/HostingModels/SocketNodeInstance.cs +++ b/src/Microsoft.AspNetCore.NodeServices/HostingModels/SocketNodeInstance.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using System.Text; using System.Threading; @@ -38,7 +39,8 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels private VirtualConnectionClient _virtualConnectionClient; public SocketNodeInstance(string projectPath, string[] watchFileExtensions, string socketAddress, - ILogger nodeInstanceOutputLogger, bool launchWithDebugging, int? debuggingPort) + ILogger nodeInstanceOutputLogger, IDictionary environmentVars, + bool launchWithDebugging, int? debuggingPort) : base( EmbeddedResourceReader.Read( typeof(SocketNodeInstance), @@ -47,6 +49,7 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels watchFileExtensions, MakeNewCommandLineOptions(socketAddress), nodeInstanceOutputLogger, + environmentVars, launchWithDebugging, debuggingPort) { diff --git a/src/Microsoft.AspNetCore.SpaServices/Prerendering/PrerenderTagHelper.cs b/src/Microsoft.AspNetCore.SpaServices/Prerendering/PrerenderTagHelper.cs index e773bbb..1baaa60 100644 --- a/src/Microsoft.AspNetCore.SpaServices/Prerendering/PrerenderTagHelper.cs +++ b/src/Microsoft.AspNetCore.SpaServices/Prerendering/PrerenderTagHelper.cs @@ -36,7 +36,7 @@ namespace Microsoft.AspNetCore.SpaServices.Prerendering _nodeServices = _fallbackNodeServices = Configuration.CreateNodeServices(new NodeServicesOptions { ProjectPath = _applicationBasePath - }); + }.AddDefaultEnvironmentVariables(hostEnv.IsDevelopment())); } } diff --git a/src/Microsoft.AspNetCore.SpaServices/Webpack/WebpackDevMiddleware.cs b/src/Microsoft.AspNetCore.SpaServices/Webpack/WebpackDevMiddleware.cs index 89d6950..5a8c06f 100644 --- a/src/Microsoft.AspNetCore.SpaServices/Webpack/WebpackDevMiddleware.cs +++ b/src/Microsoft.AspNetCore.SpaServices/Webpack/WebpackDevMiddleware.cs @@ -35,16 +35,8 @@ namespace Microsoft.AspNetCore.Builder "To enable ReactHotModuleReplacement, you must also enable HotModuleReplacement."); } - string projectPath; - if (options.ProjectPath == null) - { - var hostEnv = (IHostingEnvironment)appBuilder.ApplicationServices.GetService(typeof(IHostingEnvironment)); - projectPath = hostEnv.ContentRootPath; - } - else - { - projectPath = options.ProjectPath; - } + var hostEnv = (IHostingEnvironment)appBuilder.ApplicationServices.GetService(typeof(IHostingEnvironment)); + var projectPath = options.ProjectPath ?? hostEnv.ContentRootPath; // Unlike other consumers of NodeServices, WebpackDevMiddleware dosen't share Node instances, nor does it // use your DI configuration. It's important for WebpackDevMiddleware to have its own private Node instance @@ -55,7 +47,7 @@ namespace Microsoft.AspNetCore.Builder { ProjectPath = projectPath, WatchFileExtensions = new string[] { } // Don't watch anything - }); + }.AddDefaultEnvironmentVariables(hostEnv.IsDevelopment())); // Get a filename matching the middleware Node script var script = EmbeddedResourceReader.Read(typeof(WebpackDevMiddleware), From 101902694392a6db9a957e1c0d61a2b8733c040b Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Thu, 18 Aug 2016 10:48:13 -0700 Subject: [PATCH 05/55] Build NuGet package containing dotnetnew templates --- templates/yeoman/.gitignore | 2 +- templates/yeoman/src/build/build.ts | 127 +++++++++++++----- .../Microsoft.AspNetCore.Spa.Templates.nuspec | 19 +++ templates/yeoman/tsconfig.json | 2 +- 4 files changed, 115 insertions(+), 35 deletions(-) create mode 100644 templates/yeoman/src/dotnetnew/Microsoft.AspNetCore.Spa.Templates.nuspec diff --git a/templates/yeoman/.gitignore b/templates/yeoman/.gitignore index 33254eb..f0cef27 100644 --- a/templates/yeoman/.gitignore +++ b/templates/yeoman/.gitignore @@ -1,3 +1,3 @@ /node_modules/ /built/ -/generator-aspnetcore-spa/ +/dist/ diff --git a/templates/yeoman/src/build/build.ts b/templates/yeoman/src/build/build.ts index ce07100..72f0f37 100644 --- a/templates/yeoman/src/build/build.ts +++ b/templates/yeoman/src/build/build.ts @@ -5,28 +5,19 @@ import * as path from 'path'; import * as _ from 'lodash'; import * as mkdirp from 'mkdirp'; import * as rimraf from 'rimraf'; +import * as childProcess from 'child_process'; -const textFileExtensions = ['.gitignore', 'template_gitignore', '.config', '.cs', '.cshtml', 'Dockerfile', '.html', '.js', '.json', '.jsx', '.md', '.ts', '.tsx', '.xproj']; +const isWindows = /^win/.test(process.platform); +const textFileExtensions = ['.gitignore', 'template_gitignore', '.config', '.cs', '.cshtml', 'Dockerfile', '.html', '.js', '.json', '.jsx', '.md', '.nuspec', '.ts', '.tsx', '.xproj']; +const yeomanGeneratorSource = './src/generator'; -const templates = { - 'angular-2': '../../templates/Angular2Spa/', - 'knockout': '../../templates/KnockoutSpa/', - 'react-redux': '../../templates/ReactReduxSpa/', - 'react': '../../templates/ReactSpa/' +const templates: { [key: string]: { dir: string, dotNetNewId: string, displayName: string } } = { + 'angular-2': { dir: '../../templates/Angular2Spa/', dotNetNewId: 'Angular', displayName: 'Angular 2' }, + 'knockout': { dir: '../../templates/KnockoutSpa/', dotNetNewId: 'Knockout', displayName: 'Knockout.js' }, + 'react-redux': { dir: '../../templates/ReactReduxSpa/', dotNetNewId: 'ReactRedux', displayName: 'React.js and Redux' }, + 'react': { dir: '../../templates/ReactSpa/', dotNetNewId: 'React', displayName: 'React.js' } }; -const contentReplacements: { from: RegExp, to: string }[] = [ - { from: /\bWebApplicationBasic\b/g, to: '<%= namePascalCase %>' }, - { from: /[0-9a-f\-]{36}<\/ProjectGuid>/g, to: '<%= projectGuid %>' }, - { from: /.*?<\/RootNamespace>/g, to: '<%= namePascalCase %>'}, - { from: /\s*/g, to: '' }, - { from: /\s*/g, to: '' }, -]; - -const filenameReplacements: { from: RegExp, to: string }[] = [ - { from: /.*\.xproj$/, to: 'tokenreplace-namePascalCase.xproj' } -]; - function isTextFile(filename: string): boolean { return textFileExtensions.indexOf(path.extname(filename).toLowerCase()) >= 0; } @@ -49,7 +40,7 @@ function listFilesExcludingGitignored(root: string): string[] { .filter(fn => gitignoreEvaluator.accepts(fn)); } -function writeTemplate(sourceRoot: string, destRoot: string) { +function writeTemplate(sourceRoot: string, destRoot: string, contentReplacements: { from: RegExp, to: string }[], filenameReplacements: { from: RegExp, to: string }[]) { listFilesExcludingGitignored(sourceRoot).forEach(fn => { let sourceContent = fs.readFileSync(path.join(sourceRoot, fn)); @@ -80,20 +71,90 @@ function copyRecursive(sourceRoot: string, destRoot: string, matchGlob: string) }); } -const outputRoot = './generator-aspnetcore-spa'; -const outputTemplatesRoot = path.join(outputRoot, 'app/templates'); -rimraf.sync(outputTemplatesRoot); +function buildYeomanNpmPackage() { + const outputRoot = './dist/generator-aspnetcore-spa'; + const outputTemplatesRoot = path.join(outputRoot, 'app/templates'); + rimraf.sync(outputTemplatesRoot); -// Copy template files -_.forEach(templates, (templateRootDir, templateName) => { - const outputDir = path.join(outputTemplatesRoot, templateName); - writeTemplate(templateRootDir, outputDir); -}); + // Copy template files + const filenameReplacements = [ + { from: /.*\.xproj$/, to: 'tokenreplace-namePascalCase.xproj' } + ]; + const contentReplacements = [ + { from: /\bWebApplicationBasic\b/g, to: '<%= namePascalCase %>' }, + { from: /[0-9a-f\-]{36}<\/ProjectGuid>/g, to: '<%= projectGuid %>' }, + { from: /.*?<\/RootNamespace>/g, to: '<%= namePascalCase %>'}, + { from: /\s*/g, to: '' }, + { from: /\s*/g, to: '' }, + ]; + _.forEach(templates, (templateConfig, templateName) => { + const outputDir = path.join(outputTemplatesRoot, templateName); + writeTemplate(templateConfig.dir, outputDir, contentReplacements, filenameReplacements); + }); -// Also copy the generator files (that's the compiled .js files, plus all other non-.ts files) -const tempRoot = './tmp'; -copyRecursive(path.join(tempRoot, 'generator'), outputRoot, '**/*.js'); -copyRecursive('./src/generator', outputRoot, '**/!(*.ts)'); + // Also copy the generator files (that's the compiled .js files, plus all other non-.ts files) + const tempRoot = './tmp'; + copyRecursive(path.join(tempRoot, 'generator'), outputRoot, '**/*.js'); + copyRecursive(yeomanGeneratorSource, outputRoot, '**/!(*.ts)'); -// Clean up -rimraf.sync(tempRoot); + // Clean up + rimraf.sync(tempRoot); +} + +function buildDotNetNewNuGetPackage() { + const outputRoot = './dist/dotnetnew'; + rimraf.sync(outputRoot); + + // Copy template files + const sourceProjectName = 'WebApplicationBasic'; + const projectGuid = '00000000-0000-0000-0000-000000000000'; + const filenameReplacements = [ + { from: /.*\.xproj$/, to: `${sourceProjectName}.xproj` }, + { from: /\btemplate_gitignore$/, to: '.gitignore' } + ]; + const contentReplacements = [ + { from: /[0-9a-f\-]{36}<\/ProjectGuid>/g, to: `${projectGuid}` }, + { from: /.*?<\/RootNamespace>/g, to: `${sourceProjectName}`}, + { from: /\s*/g, to: '' }, + { from: /\s*/g, to: '' }, + ]; + _.forEach(templates, (templateConfig, templateName) => { + const templateOutputDir = path.join(outputRoot, 'templates', templateName); + const templateOutputProjectDir = path.join(templateOutputDir, sourceProjectName); + writeTemplate(templateConfig.dir, templateOutputProjectDir, contentReplacements, filenameReplacements); + + // Add a .netnew.json file + fs.writeFileSync(path.join(templateOutputDir, '.netnew.json'), JSON.stringify({ + author: 'Microsoft', + classifications: [ 'Standard>>Quick Starts' ], + name: `ASP.NET Core SPA with ${templateConfig.displayName}`, + groupIdentity: `Microsoft.AspNetCore.Spa.${templateConfig.dotNetNewId}`, + identity: `Microsoft.AspNetCore.Spa.${templateConfig.dotNetNewId}`, + shortName: `aspnetcorespa-${templateConfig.dotNetNewId.toLowerCase()}`, + tags: { language: 'C#' }, + guids: [ projectGuid ], + sourceName: sourceProjectName + }, null, 2)); + }); + + // Invoke NuGet to create the final package + const yeomanPackageVersion = JSON.parse(fs.readFileSync(path.join(yeomanGeneratorSource, 'package.json'), 'utf8')).version; + writeTemplate('./src/dotnetnew', outputRoot, [ + { from: /\{version\}/g, to: yeomanPackageVersion }, + ], []); + const nugetExe = path.join(process.cwd(), './bin/NuGet.exe'); + const nugetStartInfo = { cwd: outputRoot, stdio: 'inherit' }; + if (isWindows) { + // Invoke NuGet.exe directly + childProcess.spawnSync(nugetExe, ['pack'], nugetStartInfo); + } else { + // Invoke via Mono (relying on that being available) + childProcess.spawnSync('mono', [nugetExe, 'pack'], nugetStartInfo); + } + + // Clean up + rimraf.sync('./tmp'); +} + +buildYeomanNpmPackage(); +buildDotNetNewNuGetPackage(); diff --git a/templates/yeoman/src/dotnetnew/Microsoft.AspNetCore.Spa.Templates.nuspec b/templates/yeoman/src/dotnetnew/Microsoft.AspNetCore.Spa.Templates.nuspec new file mode 100644 index 0000000..46c0011 --- /dev/null +++ b/templates/yeoman/src/dotnetnew/Microsoft.AspNetCore.Spa.Templates.nuspec @@ -0,0 +1,19 @@ + + + + Microsoft.AspNetCore.Spa.Templates + {version} + Class Library and Console Application Templates for .NET Core + Microsoft + false + My package description. + + + + + + + + + + \ No newline at end of file diff --git a/templates/yeoman/tsconfig.json b/templates/yeoman/tsconfig.json index 107dfae..b3a8759 100644 --- a/templates/yeoman/tsconfig.json +++ b/templates/yeoman/tsconfig.json @@ -7,6 +7,6 @@ }, "exclude": [ "node_modules", - "generator-aspnetcore-spa" + "dist" ] } From d928ef4f123c343716f9fb631c8bbf0d485287f9 Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Thu, 18 Aug 2016 10:58:04 -0700 Subject: [PATCH 06/55] Rename folders since the template package generator now creates a package for "dotnet new" as well as Yeoman --- templates/{yeoman => package-builder}/.gitignore | 0 templates/{yeoman => package-builder}/README.md | 0 templates/{yeoman => package-builder}/package.json | 2 +- templates/{yeoman => package-builder}/src/build/build.ts | 4 ++-- .../src/dotnetnew/Microsoft.AspNetCore.Spa.Templates.nuspec | 0 .../src/generator => package-builder/src/yeoman}/.gitignore | 0 .../src/generator => package-builder/src/yeoman}/app/index.ts | 0 .../src/generator => package-builder/src/yeoman}/package.json | 0 templates/{yeoman => package-builder}/tsconfig.json | 0 templates/{yeoman => package-builder}/tsd.json | 0 .../typings/gitignore-parser/gitignore-parser.d.ts | 0 templates/{yeoman => package-builder}/typings/glob/glob.d.ts | 0 .../{yeoman => package-builder}/typings/lodash/lodash.d.ts | 0 .../typings/minimatch/minimatch.d.ts | 0 .../{yeoman => package-builder}/typings/mkdirp/mkdirp.d.ts | 0 .../typings/node-uuid/node-uuid-base.d.ts | 0 .../typings/node-uuid/node-uuid-cjs.d.ts | 0 .../typings/node-uuid/node-uuid.d.ts | 0 templates/{yeoman => package-builder}/typings/node/node.d.ts | 0 .../{yeoman => package-builder}/typings/rimraf/rimraf.d.ts | 0 templates/{yeoman => package-builder}/typings/tsd.d.ts | 0 .../typings/yeoman-generator/yeoman-generator.d.ts | 0 22 files changed, 3 insertions(+), 3 deletions(-) rename templates/{yeoman => package-builder}/.gitignore (100%) rename templates/{yeoman => package-builder}/README.md (100%) rename templates/{yeoman => package-builder}/package.json (82%) rename templates/{yeoman => package-builder}/src/build/build.ts (98%) rename templates/{yeoman => package-builder}/src/dotnetnew/Microsoft.AspNetCore.Spa.Templates.nuspec (100%) rename templates/{yeoman/src/generator => package-builder/src/yeoman}/.gitignore (100%) rename templates/{yeoman/src/generator => package-builder/src/yeoman}/app/index.ts (100%) rename templates/{yeoman/src/generator => package-builder/src/yeoman}/package.json (100%) rename templates/{yeoman => package-builder}/tsconfig.json (100%) rename templates/{yeoman => package-builder}/tsd.json (100%) rename templates/{yeoman => package-builder}/typings/gitignore-parser/gitignore-parser.d.ts (100%) rename templates/{yeoman => package-builder}/typings/glob/glob.d.ts (100%) rename templates/{yeoman => package-builder}/typings/lodash/lodash.d.ts (100%) rename templates/{yeoman => package-builder}/typings/minimatch/minimatch.d.ts (100%) rename templates/{yeoman => package-builder}/typings/mkdirp/mkdirp.d.ts (100%) rename templates/{yeoman => package-builder}/typings/node-uuid/node-uuid-base.d.ts (100%) rename templates/{yeoman => package-builder}/typings/node-uuid/node-uuid-cjs.d.ts (100%) rename templates/{yeoman => package-builder}/typings/node-uuid/node-uuid.d.ts (100%) rename templates/{yeoman => package-builder}/typings/node/node.d.ts (100%) rename templates/{yeoman => package-builder}/typings/rimraf/rimraf.d.ts (100%) rename templates/{yeoman => package-builder}/typings/tsd.d.ts (100%) rename templates/{yeoman => package-builder}/typings/yeoman-generator/yeoman-generator.d.ts (100%) diff --git a/templates/yeoman/.gitignore b/templates/package-builder/.gitignore similarity index 100% rename from templates/yeoman/.gitignore rename to templates/package-builder/.gitignore diff --git a/templates/yeoman/README.md b/templates/package-builder/README.md similarity index 100% rename from templates/yeoman/README.md rename to templates/package-builder/README.md diff --git a/templates/yeoman/package.json b/templates/package-builder/package.json similarity index 82% rename from templates/yeoman/package.json rename to templates/package-builder/package.json index e97d33f..b61181b 100644 --- a/templates/yeoman/package.json +++ b/templates/package-builder/package.json @@ -1,7 +1,7 @@ { "name": "generator-aspnetcore-spa-generator", "version": "1.0.0", - "description": "Creates the Yeoman generator for ASP.NET Core SPA templates", + "description": "Creates the Yeoman generator and 'dotnet new' package for ASP.NET Core SPA templates", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", diff --git a/templates/yeoman/src/build/build.ts b/templates/package-builder/src/build/build.ts similarity index 98% rename from templates/yeoman/src/build/build.ts rename to templates/package-builder/src/build/build.ts index 72f0f37..546ff89 100644 --- a/templates/yeoman/src/build/build.ts +++ b/templates/package-builder/src/build/build.ts @@ -9,7 +9,7 @@ import * as childProcess from 'child_process'; const isWindows = /^win/.test(process.platform); const textFileExtensions = ['.gitignore', 'template_gitignore', '.config', '.cs', '.cshtml', 'Dockerfile', '.html', '.js', '.json', '.jsx', '.md', '.nuspec', '.ts', '.tsx', '.xproj']; -const yeomanGeneratorSource = './src/generator'; +const yeomanGeneratorSource = './src/yeoman'; const templates: { [key: string]: { dir: string, dotNetNewId: string, displayName: string } } = { 'angular-2': { dir: '../../templates/Angular2Spa/', dotNetNewId: 'Angular', displayName: 'Angular 2' }, @@ -94,7 +94,7 @@ function buildYeomanNpmPackage() { // Also copy the generator files (that's the compiled .js files, plus all other non-.ts files) const tempRoot = './tmp'; - copyRecursive(path.join(tempRoot, 'generator'), outputRoot, '**/*.js'); + copyRecursive(path.join(tempRoot, 'yeoman'), outputRoot, '**/*.js'); copyRecursive(yeomanGeneratorSource, outputRoot, '**/!(*.ts)'); // Clean up diff --git a/templates/yeoman/src/dotnetnew/Microsoft.AspNetCore.Spa.Templates.nuspec b/templates/package-builder/src/dotnetnew/Microsoft.AspNetCore.Spa.Templates.nuspec similarity index 100% rename from templates/yeoman/src/dotnetnew/Microsoft.AspNetCore.Spa.Templates.nuspec rename to templates/package-builder/src/dotnetnew/Microsoft.AspNetCore.Spa.Templates.nuspec diff --git a/templates/yeoman/src/generator/.gitignore b/templates/package-builder/src/yeoman/.gitignore similarity index 100% rename from templates/yeoman/src/generator/.gitignore rename to templates/package-builder/src/yeoman/.gitignore diff --git a/templates/yeoman/src/generator/app/index.ts b/templates/package-builder/src/yeoman/app/index.ts similarity index 100% rename from templates/yeoman/src/generator/app/index.ts rename to templates/package-builder/src/yeoman/app/index.ts diff --git a/templates/yeoman/src/generator/package.json b/templates/package-builder/src/yeoman/package.json similarity index 100% rename from templates/yeoman/src/generator/package.json rename to templates/package-builder/src/yeoman/package.json diff --git a/templates/yeoman/tsconfig.json b/templates/package-builder/tsconfig.json similarity index 100% rename from templates/yeoman/tsconfig.json rename to templates/package-builder/tsconfig.json diff --git a/templates/yeoman/tsd.json b/templates/package-builder/tsd.json similarity index 100% rename from templates/yeoman/tsd.json rename to templates/package-builder/tsd.json diff --git a/templates/yeoman/typings/gitignore-parser/gitignore-parser.d.ts b/templates/package-builder/typings/gitignore-parser/gitignore-parser.d.ts similarity index 100% rename from templates/yeoman/typings/gitignore-parser/gitignore-parser.d.ts rename to templates/package-builder/typings/gitignore-parser/gitignore-parser.d.ts diff --git a/templates/yeoman/typings/glob/glob.d.ts b/templates/package-builder/typings/glob/glob.d.ts similarity index 100% rename from templates/yeoman/typings/glob/glob.d.ts rename to templates/package-builder/typings/glob/glob.d.ts diff --git a/templates/yeoman/typings/lodash/lodash.d.ts b/templates/package-builder/typings/lodash/lodash.d.ts similarity index 100% rename from templates/yeoman/typings/lodash/lodash.d.ts rename to templates/package-builder/typings/lodash/lodash.d.ts diff --git a/templates/yeoman/typings/minimatch/minimatch.d.ts b/templates/package-builder/typings/minimatch/minimatch.d.ts similarity index 100% rename from templates/yeoman/typings/minimatch/minimatch.d.ts rename to templates/package-builder/typings/minimatch/minimatch.d.ts diff --git a/templates/yeoman/typings/mkdirp/mkdirp.d.ts b/templates/package-builder/typings/mkdirp/mkdirp.d.ts similarity index 100% rename from templates/yeoman/typings/mkdirp/mkdirp.d.ts rename to templates/package-builder/typings/mkdirp/mkdirp.d.ts diff --git a/templates/yeoman/typings/node-uuid/node-uuid-base.d.ts b/templates/package-builder/typings/node-uuid/node-uuid-base.d.ts similarity index 100% rename from templates/yeoman/typings/node-uuid/node-uuid-base.d.ts rename to templates/package-builder/typings/node-uuid/node-uuid-base.d.ts diff --git a/templates/yeoman/typings/node-uuid/node-uuid-cjs.d.ts b/templates/package-builder/typings/node-uuid/node-uuid-cjs.d.ts similarity index 100% rename from templates/yeoman/typings/node-uuid/node-uuid-cjs.d.ts rename to templates/package-builder/typings/node-uuid/node-uuid-cjs.d.ts diff --git a/templates/yeoman/typings/node-uuid/node-uuid.d.ts b/templates/package-builder/typings/node-uuid/node-uuid.d.ts similarity index 100% rename from templates/yeoman/typings/node-uuid/node-uuid.d.ts rename to templates/package-builder/typings/node-uuid/node-uuid.d.ts diff --git a/templates/yeoman/typings/node/node.d.ts b/templates/package-builder/typings/node/node.d.ts similarity index 100% rename from templates/yeoman/typings/node/node.d.ts rename to templates/package-builder/typings/node/node.d.ts diff --git a/templates/yeoman/typings/rimraf/rimraf.d.ts b/templates/package-builder/typings/rimraf/rimraf.d.ts similarity index 100% rename from templates/yeoman/typings/rimraf/rimraf.d.ts rename to templates/package-builder/typings/rimraf/rimraf.d.ts diff --git a/templates/yeoman/typings/tsd.d.ts b/templates/package-builder/typings/tsd.d.ts similarity index 100% rename from templates/yeoman/typings/tsd.d.ts rename to templates/package-builder/typings/tsd.d.ts diff --git a/templates/yeoman/typings/yeoman-generator/yeoman-generator.d.ts b/templates/package-builder/typings/yeoman-generator/yeoman-generator.d.ts similarity index 100% rename from templates/yeoman/typings/yeoman-generator/yeoman-generator.d.ts rename to templates/package-builder/typings/yeoman-generator/yeoman-generator.d.ts From 9e714b61fe751bdfd59933ad9806f1626437b858 Mon Sep 17 00:00:00 2001 From: Mark Pieszak Date: Wed, 17 Aug 2016 19:06:16 -0400 Subject: [PATCH 07/55] Bug(KnockoutSpa): Router TS issue TS erroring out (unable to build) due to `crossroads.normalizeFn = crossroads.NORM_AS_OBJECT;` <-- left hand assignment ``` ERROR in ./ClientApp/router.ts (21,9): error TS2450: Left-hand side of assignment expression cannot be a constant or a read-only property. ``` --- templates/KnockoutSpa/ClientApp/router.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/KnockoutSpa/ClientApp/router.ts b/templates/KnockoutSpa/ClientApp/router.ts index 526394e..12e65c1 100644 --- a/templates/KnockoutSpa/ClientApp/router.ts +++ b/templates/KnockoutSpa/ClientApp/router.ts @@ -1,5 +1,5 @@ import * as ko from 'knockout'; -import * as crossroads from 'crossroads'; +var crossroads = require('crossroads'); // This module configures crossroads.js, a routing library. If you prefer, you // can use any other routing library (or none at all) as Knockout is designed to From 89034b59c517794968c79b647b3566bb2293af72 Mon Sep 17 00:00:00 2001 From: Mark Pieszak Date: Thu, 18 Aug 2016 15:16:13 -0400 Subject: [PATCH 08/55] Change to mix import/require style --- templates/KnockoutSpa/ClientApp/router.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/KnockoutSpa/ClientApp/router.ts b/templates/KnockoutSpa/ClientApp/router.ts index 12e65c1..46000e7 100644 --- a/templates/KnockoutSpa/ClientApp/router.ts +++ b/templates/KnockoutSpa/ClientApp/router.ts @@ -1,5 +1,5 @@ import * as ko from 'knockout'; -var crossroads = require('crossroads'); +import crossroads = require('crossroads'); // This module configures crossroads.js, a routing library. If you prefer, you // can use any other routing library (or none at all) as Knockout is designed to From 6d8767d14158734e26fd3432cc279583b798d90d Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Thu, 18 Aug 2016 13:49:20 -0700 Subject: [PATCH 09/55] Tweak WebpackDevMiddleware.cs to avoid unnecessary Task.Yield() --- .../Webpack/WebpackDevMiddleware.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.AspNetCore.SpaServices/Webpack/WebpackDevMiddleware.cs b/src/Microsoft.AspNetCore.SpaServices/Webpack/WebpackDevMiddleware.cs index 5a8c06f..1a0c9bb 100644 --- a/src/Microsoft.AspNetCore.SpaServices/Webpack/WebpackDevMiddleware.cs +++ b/src/Microsoft.AspNetCore.SpaServices/Webpack/WebpackDevMiddleware.cs @@ -81,12 +81,12 @@ namespace Microsoft.AspNetCore.Builder // sees, not "localhost", so that it works even when you're not running on localhost (e.g., Docker). appBuilder.Map(WebpackHotMiddlewareEndpoint, builder => { - builder.Use(next => async ctx => + builder.Use(next => ctx => { var hostname = ctx.Request.Host.Host; ctx.Response.Redirect( $"{WebpackDevMiddlewareScheme}://{hostname}:{devServerInfo.Port.ToString()}{WebpackHotMiddlewareEndpoint}"); - await Task.Yield(); + return Task.FromResult(0); }); }); } From 0ad048393569abb126e88e2722bb45d24de1cd24 Mon Sep 17 00:00:00 2001 From: Geir Sagberg Date: Fri, 12 Aug 2016 11:42:19 +0200 Subject: [PATCH 10/55] Update aspnet-webpack-react to React 15 ^15.0.0 is enough Update ReactGrid example to work with newest Griddle version Several fixes to stop build.sh from failing --- .gitignore | 1 + .../ReactApp/components/PeopleGrid.jsx | 4 +- samples/react/ReactGrid/package.json | 54 +++++++++---------- .../npm/package.json | 3 +- .../npm/redux-typed/package.json | 3 +- .../npm/redux-typed/src/StrongActions.ts | 2 +- .../npm/aspnet-prerendering/package.json | 3 +- .../npm/aspnet-webpack-react/package.json | 5 +- .../npm/aspnet-webpack/package.json | 4 +- .../npm/domain-task/package.json | 3 +- 10 files changed, 45 insertions(+), 37 deletions(-) diff --git a/.gitignore b/.gitignore index 62ebab8..06cc11a 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,4 @@ npm-debug.log # repo have to be excluded here. /templates/*/node_modules/ /templates/*/wwwroot/dist/ +.vscode/ diff --git a/samples/react/ReactGrid/ReactApp/components/PeopleGrid.jsx b/samples/react/ReactGrid/ReactApp/components/PeopleGrid.jsx index a9dbec1..e3cd3fc 100644 --- a/samples/react/ReactGrid/ReactApp/components/PeopleGrid.jsx +++ b/samples/react/ReactGrid/ReactApp/components/PeopleGrid.jsx @@ -5,6 +5,8 @@ import { fakeData } from '../data/fakeData.js'; import { columnMeta } from '../data/columnMeta.jsx'; const resultsPerPage = 10; +const fakeDataWithAction = fakeData.map(data => Object.assign(data, {actions: ''})); + export class PeopleGrid extends React.Component { render() { var pageIndex = this.props.params ? (this.props.params.pageIndex || 1) - 1 : 0; @@ -12,7 +14,7 @@ export class PeopleGrid extends React.Component {

People

- x.columnName)} columnMetadata={columnMeta} resultsPerPage={resultsPerPage} diff --git a/samples/react/ReactGrid/package.json b/samples/react/ReactGrid/package.json index 123cc47..9329a55 100644 --- a/samples/react/ReactGrid/package.json +++ b/samples/react/ReactGrid/package.json @@ -2,38 +2,38 @@ "name": "ReactExample", "version": "0.0.0", "dependencies": { - "babel-core": "^6.4.5", - "bootstrap": "^3.3.5", - "domain-task": "^2.0.0", - "formsy-react": "^0.17.0", - "formsy-react-components": "^0.6.3", - "griddle-react": "^0.3.1", - "history": "^1.12.6", + "babel-core": "^6.13.2", + "bootstrap": "^3.3.7", + "domain-task": "^2.0.1", + "formsy-react": "^0.18.1", + "formsy-react-components": "^0.8.1", + "griddle-react": "^0.6.1", + "history": "^3.0.0", "memory-fs": "^0.3.0", - "react": "^0.14.7", - "react-dom": "^0.14.7", - "react-router": "^2.0.0-rc5", - "require-from-string": "^1.1.0", + "react": "^15.3.0", + "react-dom": "^15.3.0", + "react-router": "^2.6.1", + "require-from-string": "^1.2.0", "underscore": "^1.8.3", "webpack-externals-plugin": "^1.0.0" }, "devDependencies": { - "aspnet-prerendering": "^1.0.0", - "aspnet-webpack": "^1.0.3", + "aspnet-prerendering": "^1.0.4", + "aspnet-webpack": "^1.0.9", "aspnet-webpack-react": "^1.0.1", - "babel-loader": "^6.2.1", - "babel-plugin-react-transform": "^2.0.0", - "babel-preset-es2015": "^6.3.13", - "babel-preset-react": "^6.3.13", - "css-loader": "^0.21.0", - "express": "^4.13.4", - "extract-text-webpack-plugin": "^0.8.2", - "file-loader": "^0.8.4", - "react-transform-hmr": "^1.0.1", - "style-loader": "^0.13.0", - "url-loader": "^0.5.6", - "webpack": "^1.12.2", - "webpack-dev-middleware": "^1.5.1", - "webpack-hot-middleware": "^2.6.4" + "babel-loader": "^6.2.4", + "babel-plugin-react-transform": "^2.0.2", + "babel-preset-es2015": "^6.13.2", + "babel-preset-react": "^6.11.1", + "css-loader": "^0.23.1", + "express": "^4.14.0", + "extract-text-webpack-plugin": "^1.0.1", + "file-loader": "^0.9.0", + "react-transform-hmr": "^1.0.4", + "style-loader": "^0.13.1", + "url-loader": "^0.5.7", + "webpack": "^1.13.1", + "webpack-dev-middleware": "^1.6.1", + "webpack-hot-middleware": "^2.12.2" } } diff --git a/src/Microsoft.AspNetCore.AngularServices/npm/package.json b/src/Microsoft.AspNetCore.AngularServices/npm/package.json index 8f7c19e..10c3f77 100644 --- a/src/Microsoft.AspNetCore.AngularServices/npm/package.json +++ b/src/Microsoft.AspNetCore.AngularServices/npm/package.json @@ -5,7 +5,7 @@ "main": "./dist/Exports", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", - "prepublish": "tsd install && tsc && node build.js" + "prepublish": "rimraf *.d.ts dist/*.d.ts && tsd install && tsc && node build.js" }, "typings": "dist/Exports", "author": "Microsoft", @@ -17,6 +17,7 @@ "devDependencies": { "es6-shim": "^0.35.0", "reflect-metadata": "^0.1.2", + "rimraf": "^2.5.4", "systemjs-builder": "^0.14.11", "typescript": "^1.8.10", "zone.js": "^0.6.10" diff --git a/src/Microsoft.AspNetCore.ReactServices/npm/redux-typed/package.json b/src/Microsoft.AspNetCore.ReactServices/npm/redux-typed/package.json index 1baee3a..b796242 100644 --- a/src/Microsoft.AspNetCore.ReactServices/npm/redux-typed/package.json +++ b/src/Microsoft.AspNetCore.ReactServices/npm/redux-typed/package.json @@ -5,12 +5,13 @@ "main": "main.js", "typings": "main.d.ts", "scripts": { - "prepublish": "tsd update && tsc && echo 'Finished building NPM package \"redux-typed\"'", + "prepublish": "rimraf *.d.ts && tsd update && tsc && echo 'Finished building NPM package \"redux-typed\"'", "test": "echo \"Error: no test specified\" && exit 1" }, "author": "Microsoft", "license": "Apache-2.0", "devDependencies": { + "rimraf": "^2.5.4", "typescript": "^1.8.10" } } diff --git a/src/Microsoft.AspNetCore.ReactServices/npm/redux-typed/src/StrongActions.ts b/src/Microsoft.AspNetCore.ReactServices/npm/redux-typed/src/StrongActions.ts index 55893bf..f14c912 100644 --- a/src/Microsoft.AspNetCore.ReactServices/npm/redux-typed/src/StrongActions.ts +++ b/src/Microsoft.AspNetCore.ReactServices/npm/redux-typed/src/StrongActions.ts @@ -38,5 +38,5 @@ export interface Reducer extends Function { } export interface ActionCreatorGeneric extends Function { - (dispatch: Dispatch, getState: () => TState): any; + (dispatch: Dispatch, getState: () => TState): any; } diff --git a/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-prerendering/package.json b/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-prerendering/package.json index 11bd4ec..156a6f1 100644 --- a/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-prerendering/package.json +++ b/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-prerendering/package.json @@ -4,7 +4,7 @@ "description": "Helpers for server-side rendering of JavaScript applications in ASP.NET Core projects. Works in conjunction with the Microsoft.AspNetCore.SpaServices NuGet package.", "main": "index.js", "scripts": { - "prepublish": "tsd update && tsc && echo 'Finished building NPM package \"aspnet-prerendering\"'", + "prepublish": "rimraf *.d.ts && tsd update && tsc && echo 'Finished building NPM package \"aspnet-prerendering\"'", "test": "echo \"Error: no test specified\" && exit 1" }, "author": "Microsoft", @@ -14,6 +14,7 @@ "es6-promise": "^3.1.2" }, "devDependencies": { + "rimraf": "^2.5.4", "typescript": "^1.8.10" } } diff --git a/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-webpack-react/package.json b/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-webpack-react/package.json index 419c058..3b9fd5e 100644 --- a/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-webpack-react/package.json +++ b/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-webpack-react/package.json @@ -4,7 +4,7 @@ "description": "Helpers for using Webpack with React in ASP.NET Core projects. Works in conjunction with the Microsoft.AspNetCore.SpaServices NuGet package.", "main": "index.js", "scripts": { - "prepublish": "tsd update && tsc && echo 'Finished building NPM package \"aspnet-webpack-react\"'", + "prepublish": "rimraf *.d.ts && tsd update && tsc && echo 'Finished building NPM package \"aspnet-webpack-react\"'", "test": "echo \"Error: no test specified\" && exit 1" }, "author": "Microsoft", @@ -15,11 +15,12 @@ "babel-plugin-react-transform": "^2.0.2", "babel-preset-es2015": "^6.6.0", "babel-preset-react": "^6.5.0", - "react": "^0.14.7", + "react": "^15.0.0", "react-transform-hmr": "^1.0.4", "webpack": "^1.12.14" }, "devDependencies": { + "rimraf": "^2.5.4", "typescript": "^1.8.10" } } diff --git a/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-webpack/package.json b/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-webpack/package.json index 8980b92..327d65d 100644 --- a/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-webpack/package.json +++ b/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-webpack/package.json @@ -19,7 +19,7 @@ "webpack-externals-plugin": "^1.0.0" }, "devDependencies": { - "typescript": "^1.8.10", - "rimraf": "^2.5.2" + "rimraf": "^2.5.4", + "typescript": "^1.8.10" } } diff --git a/src/Microsoft.AspNetCore.SpaServices/npm/domain-task/package.json b/src/Microsoft.AspNetCore.SpaServices/npm/domain-task/package.json index 0bceb71..a55fcd8 100644 --- a/src/Microsoft.AspNetCore.SpaServices/npm/domain-task/package.json +++ b/src/Microsoft.AspNetCore.SpaServices/npm/domain-task/package.json @@ -4,7 +4,7 @@ "description": "Tracks outstanding operations for a logical thread of execution", "main": "index.js", "scripts": { - "prepublish": "tsd update && tsc && echo 'Finished building NPM package \"domain-task\"'", + "prepublish": "rimraf *.d.ts && tsd update && tsc && echo 'Finished building NPM package \"domain-task\"'", "test": "echo \"Error: no test specified\" && exit 1" }, "author": "Microsoft", @@ -14,6 +14,7 @@ "isomorphic-fetch": "^2.2.1" }, "devDependencies": { + "rimraf": "^2.5.4", "typescript": "^1.8.10" } } From 0a0afed84b904045088437177952818bde22c49d Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Thu, 18 Aug 2016 13:57:49 -0700 Subject: [PATCH 11/55] Add comment about why the 'actions' property is being patched on --- samples/react/ReactGrid/ReactApp/components/PeopleGrid.jsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/samples/react/ReactGrid/ReactApp/components/PeopleGrid.jsx b/samples/react/ReactGrid/ReactApp/components/PeopleGrid.jsx index e3cd3fc..6ce0ade 100644 --- a/samples/react/ReactGrid/ReactApp/components/PeopleGrid.jsx +++ b/samples/react/ReactGrid/ReactApp/components/PeopleGrid.jsx @@ -5,7 +5,9 @@ import { fakeData } from '../data/fakeData.js'; import { columnMeta } from '../data/columnMeta.jsx'; const resultsPerPage = 10; -const fakeDataWithAction = fakeData.map(data => Object.assign(data, {actions: ''})); +// Griddle requires each row to have a property matching each column, even if you're not displaying +// any data from the row in that column +fakeData.forEach(row => { row.actions = ''; }); export class PeopleGrid extends React.Component { render() { @@ -14,7 +16,7 @@ export class PeopleGrid extends React.Component {

People

- x.columnName)} columnMetadata={columnMeta} resultsPerPage={resultsPerPage} From a4e3360e65361c89f5fc7b76b11459b810d54f2f Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Thu, 18 Aug 2016 14:07:11 -0700 Subject: [PATCH 12/55] Bump aspnet-webpack-react package version --- .../npm/aspnet-webpack-react/package.json | 2 +- templates/ReactReduxSpa/package.json | 2 +- templates/ReactSpa/package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-webpack-react/package.json b/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-webpack-react/package.json index 3b9fd5e..1f0a94e 100644 --- a/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-webpack-react/package.json +++ b/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-webpack-react/package.json @@ -1,6 +1,6 @@ { "name": "aspnet-webpack-react", - "version": "1.0.1", + "version": "1.0.2", "description": "Helpers for using Webpack with React in ASP.NET Core projects. Works in conjunction with the Microsoft.AspNetCore.SpaServices NuGet package.", "main": "index.js", "scripts": { diff --git a/templates/ReactReduxSpa/package.json b/templates/ReactReduxSpa/package.json index a1d53df..82fb491 100644 --- a/templates/ReactReduxSpa/package.json +++ b/templates/ReactReduxSpa/package.json @@ -3,7 +3,7 @@ "version": "0.0.0", "devDependencies": { "aspnet-webpack": "^1.0.6", - "aspnet-webpack-react": "^1.0.1", + "aspnet-webpack-react": "^1.0.2", "babel-loader": "^6.2.3", "babel-preset-es2015": "^6.5.0", "babel-preset-react": "^6.5.0", diff --git a/templates/ReactSpa/package.json b/templates/ReactSpa/package.json index 9776cdb..a66c99d 100644 --- a/templates/ReactSpa/package.json +++ b/templates/ReactSpa/package.json @@ -3,7 +3,7 @@ "version": "0.0.0", "devDependencies": { "aspnet-webpack": "^1.0.6", - "aspnet-webpack-react": "^1.0.0", + "aspnet-webpack-react": "^1.0.2", "babel-loader": "^6.2.3", "babel-preset-es2015": "^6.5.0", "babel-preset-react": "^6.5.0", From 22da55a47310384aaf926f31ec342b248b7a568f Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Thu, 18 Aug 2016 14:07:53 -0700 Subject: [PATCH 13/55] Bump generator-aspnetcore-spa package version --- templates/package-builder/src/yeoman/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/package-builder/src/yeoman/package.json b/templates/package-builder/src/yeoman/package.json index 839ed75..8202518 100644 --- a/templates/package-builder/src/yeoman/package.json +++ b/templates/package-builder/src/yeoman/package.json @@ -1,6 +1,6 @@ { "name": "generator-aspnetcore-spa", - "version": "0.2.3", + "version": "0.2.4", "description": "Single-Page App templates for ASP.NET Core", "author": "Microsoft", "license": "Apache-2.0", From 5bc47aacd8484cc43d4e8a861b9e60b7dc7582dc Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Fri, 19 Aug 2016 01:08:09 +0100 Subject: [PATCH 14/55] Tweaks to .xproj files made automatically by VS --- samples/misc/Webpack/Webpack.xproj | 4 ++-- samples/react/MusicStore/MusicStore.xproj | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/samples/misc/Webpack/Webpack.xproj b/samples/misc/Webpack/Webpack.xproj index 5f9cc8f..49bc6e4 100644 --- a/samples/misc/Webpack/Webpack.xproj +++ b/samples/misc/Webpack/Webpack.xproj @@ -10,11 +10,11 @@ a8905301-8492-42fd-9e83-f715a0fdc3a2 Webpack ..\..\..\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/MusicStore/MusicStore.xproj b/samples/react/MusicStore/MusicStore.xproj index 3096e79..672fd3f 100644 --- a/samples/react/MusicStore/MusicStore.xproj +++ b/samples/react/MusicStore/MusicStore.xproj @@ -10,11 +10,11 @@ c870a92c-9e3f-4bf2-82b8-5758545a8b7c MusicStore ..\..\..\JavaScriptServices.sln\artifacts\obj\$(MSBuildProjectName) - ..\..\..\JavaScriptServices.sln\artifacts\bin\$(MSBuildProjectName)\ + .\bin\ 2.0 2018 - + \ No newline at end of file From 48eb2b7a05e861f4281b05e83ce62774bd0d74f3 Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Fri, 19 Aug 2016 01:08:57 +0100 Subject: [PATCH 15/55] In KnockoutSpa, use isomorphic-fetch for IE/Edge compatibility --- .../components/fetch-data/fetch-data.ts | 1 + templates/KnockoutSpa/package.json | 1 + templates/KnockoutSpa/tsd.json | 6 +- .../isomorphic-fetch/isomorphic-fetch.d.ts | 119 ++++++++++++++++++ templates/KnockoutSpa/typings/tsd.d.ts | 2 +- .../typings/whatwg-fetch/whatwg-fetch.d.ts | 85 ------------- .../KnockoutSpa/webpack.config.vendor.js | 2 +- 7 files changed, 126 insertions(+), 90 deletions(-) create mode 100644 templates/KnockoutSpa/typings/isomorphic-fetch/isomorphic-fetch.d.ts delete mode 100644 templates/KnockoutSpa/typings/whatwg-fetch/whatwg-fetch.d.ts diff --git a/templates/KnockoutSpa/ClientApp/components/fetch-data/fetch-data.ts b/templates/KnockoutSpa/ClientApp/components/fetch-data/fetch-data.ts index deab068..3dc781c 100644 --- a/templates/KnockoutSpa/ClientApp/components/fetch-data/fetch-data.ts +++ b/templates/KnockoutSpa/ClientApp/components/fetch-data/fetch-data.ts @@ -1,4 +1,5 @@ import * as ko from 'knockout'; +import * as fetch from 'isomorphic-fetch'; interface WeatherForecast { dateFormatted: string; diff --git a/templates/KnockoutSpa/package.json b/templates/KnockoutSpa/package.json index 9de9738..96a49bd 100644 --- a/templates/KnockoutSpa/package.json +++ b/templates/KnockoutSpa/package.json @@ -11,6 +11,7 @@ "extract-text-webpack-plugin": "^1.0.1", "file-loader": "^0.8.5", "history": "^2.0.1", + "isomorphic-fetch": "^2.2.1", "jquery": "^2.2.1", "knockout": "^3.4.0", "raw-loader": "^0.5.1", diff --git a/templates/KnockoutSpa/tsd.json b/templates/KnockoutSpa/tsd.json index 0e5592c..cde96c4 100644 --- a/templates/KnockoutSpa/tsd.json +++ b/templates/KnockoutSpa/tsd.json @@ -5,9 +5,6 @@ "path": "typings", "bundle": "typings/tsd.d.ts", "installed": { - "whatwg-fetch/whatwg-fetch.d.ts": { - "commit": "dade4414712ce84e3c63393f1aae407e9e7e6af7" - }, "knockout/knockout.d.ts": { "commit": "9f0f926a12026287b5a4a229e5672c01e7549313" }, @@ -28,6 +25,9 @@ }, "js-signals/js-signals.d.ts": { "commit": "9f0f926a12026287b5a4a229e5672c01e7549313" + }, + "isomorphic-fetch/isomorphic-fetch.d.ts": { + "commit": "57ec5fbb76060329c10959d449eb1d4e70b15a65" } } } diff --git a/templates/KnockoutSpa/typings/isomorphic-fetch/isomorphic-fetch.d.ts b/templates/KnockoutSpa/typings/isomorphic-fetch/isomorphic-fetch.d.ts new file mode 100644 index 0000000..19754db --- /dev/null +++ b/templates/KnockoutSpa/typings/isomorphic-fetch/isomorphic-fetch.d.ts @@ -0,0 +1,119 @@ +// Type definitions for isomorphic-fetch +// Project: https://github.com/matthew-andrews/isomorphic-fetch +// Definitions by: Todd Lucas +// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped + +declare enum RequestContext { + "audio", "beacon", "cspreport", "download", "embed", "eventsource", + "favicon", "fetch", "font", "form", "frame", "hyperlink", "iframe", + "image", "imageset", "import", "internal", "location", "manifest", + "object", "ping", "plugin", "prefetch", "script", "serviceworker", + "sharedworker", "subresource", "style", "track", "video", "worker", + "xmlhttprequest", "xslt" +} +declare enum RequestMode { "same-origin", "no-cors", "cors" } +declare enum RequestCredentials { "omit", "same-origin", "include" } +declare enum RequestCache { + "default", "no-store", "reload", "no-cache", "force-cache", + "only-if-cached" +} +declare enum ResponseType { "basic", "cors", "default", "error", "opaque" } + +declare type HeaderInit = Headers | Array; +declare type BodyInit = ArrayBuffer | ArrayBufferView | Blob | FormData | string; +declare type RequestInfo = Request | string; + +interface RequestInit { + method?: string; + headers?: HeaderInit | { [index: string]: string }; + body?: BodyInit; + mode?: string | RequestMode; + credentials?: string | RequestCredentials; + cache?: string | RequestCache; +} + +interface IHeaders { + get(name: string): string; + getAll(name: string): Array; + has(name: string): boolean; +} + +declare class Headers implements IHeaders { + append(name: string, value: string): void; + delete(name: string):void; + get(name: string): string; + getAll(name: string): Array; + has(name: string): boolean; + set(name: string, value: string): void; +} + +interface IBody { + bodyUsed: boolean; + arrayBuffer(): Promise; + blob(): Promise; + formData(): Promise; + json(): Promise; + json(): Promise; + text(): Promise; +} + +declare class Body implements IBody { + bodyUsed: boolean; + arrayBuffer(): Promise; + blob(): Promise; + formData(): Promise; + json(): Promise; + json(): Promise; + text(): Promise; +} + +interface IRequest extends IBody { + method: string; + url: string; + headers: Headers; + context: string | RequestContext; + referrer: string; + mode: string | RequestMode; + credentials: string | RequestCredentials; + cache: string | RequestCache; +} + +declare class Request extends Body implements IRequest { + constructor(input: string | Request, init?: RequestInit); + method: string; + url: string; + headers: Headers; + context: string | RequestContext; + referrer: string; + mode: string | RequestMode; + credentials: string | RequestCredentials; + cache: string | RequestCache; +} + +interface IResponse extends IBody { + url: string; + status: number; + statusText: string; + ok: boolean; + headers: IHeaders; + type: string | ResponseType; + size: number; + timeout: number; + redirect(url: string, status: number): IResponse; + error(): IResponse; + clone(): IResponse; +} + +interface IFetchStatic { + Promise: any; + Headers: IHeaders + Request: IRequest; + Response: IResponse; + (url: string | IRequest, init?: RequestInit): Promise; +} + +declare var fetch: IFetchStatic; + +declare module "isomorphic-fetch" { + export = fetch; +} diff --git a/templates/KnockoutSpa/typings/tsd.d.ts b/templates/KnockoutSpa/typings/tsd.d.ts index c0e628b..6a8ceca 100644 --- a/templates/KnockoutSpa/typings/tsd.d.ts +++ b/templates/KnockoutSpa/typings/tsd.d.ts @@ -1,8 +1,8 @@ /// /// /// -/// /// /// /// /// +/// diff --git a/templates/KnockoutSpa/typings/whatwg-fetch/whatwg-fetch.d.ts b/templates/KnockoutSpa/typings/whatwg-fetch/whatwg-fetch.d.ts deleted file mode 100644 index 64dd904..0000000 --- a/templates/KnockoutSpa/typings/whatwg-fetch/whatwg-fetch.d.ts +++ /dev/null @@ -1,85 +0,0 @@ -// Type definitions for fetch API -// Project: https://github.com/github/fetch -// Definitions by: Ryan Graham -// Definitions: https://github.com/borisyankov/DefinitelyTyped - -declare class Request extends Body { - constructor(input: string|Request, init?:RequestInit); - method: string; - url: string; - headers: Headers; - context: string|RequestContext; - referrer: string; - mode: string|RequestMode; - credentials: string|RequestCredentials; - cache: string|RequestCache; -} - -interface RequestInit { - method?: string; - headers?: HeaderInit|{ [index: string]: string }; - body?: BodyInit; - mode?: string|RequestMode; - credentials?: string|RequestCredentials; - cache?: string|RequestCache; -} - -declare enum RequestContext { - "audio", "beacon", "cspreport", "download", "embed", "eventsource", "favicon", "fetch", - "font", "form", "frame", "hyperlink", "iframe", "image", "imageset", "import", - "internal", "location", "manifest", "object", "ping", "plugin", "prefetch", "script", - "serviceworker", "sharedworker", "subresource", "style", "track", "video", "worker", - "xmlhttprequest", "xslt" -} -declare enum RequestMode { "same-origin", "no-cors", "cors" } -declare enum RequestCredentials { "omit", "same-origin", "include" } -declare enum RequestCache { "default", "no-store", "reload", "no-cache", "force-cache", "only-if-cached" } - -declare class Headers { - append(name: string, value: string): void; - delete(name: string):void; - get(name: string): string; - getAll(name: string): Array; - has(name: string): boolean; - set(name: string, value: string): void; -} - -declare class Body { - bodyUsed: boolean; - arrayBuffer(): Promise; - blob(): Promise; - formData(): Promise; - json(): Promise; - json(): Promise; - text(): Promise; -} -declare class Response extends Body { - constructor(body?: BodyInit, init?: ResponseInit); - error(): Response; - redirect(url: string, status: number): Response; - type: string|ResponseType; - url: string; - status: number; - ok: boolean; - statusText: string; - headers: Headers; - clone(): Response; -} - -declare enum ResponseType { "basic", "cors", "default", "error", "opaque" } - -interface ResponseInit { - status: number; - statusText?: string; - headers?: HeaderInit; -} - -declare type HeaderInit = Headers|Array; -declare type BodyInit = Blob|FormData|string; -declare type RequestInfo = Request|string; - -interface Window { - fetch(url: string|Request, init?: RequestInit): Promise; -} - -declare var fetch: typeof window.fetch; diff --git a/templates/KnockoutSpa/webpack.config.vendor.js b/templates/KnockoutSpa/webpack.config.vendor.js index 3637579..6c212ac 100644 --- a/templates/KnockoutSpa/webpack.config.vendor.js +++ b/templates/KnockoutSpa/webpack.config.vendor.js @@ -15,7 +15,7 @@ module.exports = { ] }, entry: { - vendor: ['bootstrap', 'bootstrap/dist/css/bootstrap.css', 'knockout', 'crossroads', 'history', 'style-loader', 'jquery'], + vendor: ['bootstrap', 'bootstrap/dist/css/bootstrap.css', 'knockout', 'crossroads', 'history', 'isomorphic-fetch', 'style-loader', 'jquery'], }, output: { path: path.join(__dirname, 'wwwroot', 'dist'), From 1ce8a2215c7572f178adc00581e31e1485a40f50 Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Fri, 19 Aug 2016 01:40:35 +0100 Subject: [PATCH 16/55] In ReactSpa, use isomorphic-fetch for IE/Edge compatibility --- .../components/fetch-data/fetch-data.ts | 2 +- .../ClientApp/components/FetchData.tsx | 1 + templates/ReactSpa/package.json | 1 + templates/ReactSpa/tsd.json | 4 +- .../isomorphic-fetch/isomorphic-fetch.d.ts | 119 ++++++++++++++++++ templates/ReactSpa/typings/tsd.d.ts | 2 +- .../typings/whatwg-fetch/whatwg-fetch.d.ts | 85 ------------- templates/ReactSpa/webpack.config.vendor.js | 2 +- 8 files changed, 126 insertions(+), 90 deletions(-) create mode 100644 templates/ReactSpa/typings/isomorphic-fetch/isomorphic-fetch.d.ts delete mode 100644 templates/ReactSpa/typings/whatwg-fetch/whatwg-fetch.d.ts diff --git a/templates/KnockoutSpa/ClientApp/components/fetch-data/fetch-data.ts b/templates/KnockoutSpa/ClientApp/components/fetch-data/fetch-data.ts index 3dc781c..a6618cd 100644 --- a/templates/KnockoutSpa/ClientApp/components/fetch-data/fetch-data.ts +++ b/templates/KnockoutSpa/ClientApp/components/fetch-data/fetch-data.ts @@ -1,5 +1,5 @@ import * as ko from 'knockout'; -import * as fetch from 'isomorphic-fetch'; +import 'isomorphic-fetch'; interface WeatherForecast { dateFormatted: string; diff --git a/templates/ReactSpa/ClientApp/components/FetchData.tsx b/templates/ReactSpa/ClientApp/components/FetchData.tsx index a010bac..2ee2f1d 100644 --- a/templates/ReactSpa/ClientApp/components/FetchData.tsx +++ b/templates/ReactSpa/ClientApp/components/FetchData.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import 'isomorphic-fetch'; interface FetchDataExampleState { forecasts: WeatherForecast[]; diff --git a/templates/ReactSpa/package.json b/templates/ReactSpa/package.json index a66c99d..9e07d1b 100644 --- a/templates/ReactSpa/package.json +++ b/templates/ReactSpa/package.json @@ -22,6 +22,7 @@ }, "dependencies": { "babel-core": "^6.5.2", + "isomorphic-fetch": "^2.2.1", "react": "^15.0.1", "react-dom": "^15.0.1", "react-router": "^2.1.1" diff --git a/templates/ReactSpa/tsd.json b/templates/ReactSpa/tsd.json index 523893d..b69d22f 100644 --- a/templates/ReactSpa/tsd.json +++ b/templates/ReactSpa/tsd.json @@ -17,8 +17,8 @@ "react-router/history.d.ts": { "commit": "dade4414712ce84e3c63393f1aae407e9e7e6af7" }, - "whatwg-fetch/whatwg-fetch.d.ts": { - "commit": "dade4414712ce84e3c63393f1aae407e9e7e6af7" + "isomorphic-fetch/isomorphic-fetch.d.ts": { + "commit": "57ec5fbb76060329c10959d449eb1d4e70b15a65" } } } diff --git a/templates/ReactSpa/typings/isomorphic-fetch/isomorphic-fetch.d.ts b/templates/ReactSpa/typings/isomorphic-fetch/isomorphic-fetch.d.ts new file mode 100644 index 0000000..19754db --- /dev/null +++ b/templates/ReactSpa/typings/isomorphic-fetch/isomorphic-fetch.d.ts @@ -0,0 +1,119 @@ +// Type definitions for isomorphic-fetch +// Project: https://github.com/matthew-andrews/isomorphic-fetch +// Definitions by: Todd Lucas +// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped + +declare enum RequestContext { + "audio", "beacon", "cspreport", "download", "embed", "eventsource", + "favicon", "fetch", "font", "form", "frame", "hyperlink", "iframe", + "image", "imageset", "import", "internal", "location", "manifest", + "object", "ping", "plugin", "prefetch", "script", "serviceworker", + "sharedworker", "subresource", "style", "track", "video", "worker", + "xmlhttprequest", "xslt" +} +declare enum RequestMode { "same-origin", "no-cors", "cors" } +declare enum RequestCredentials { "omit", "same-origin", "include" } +declare enum RequestCache { + "default", "no-store", "reload", "no-cache", "force-cache", + "only-if-cached" +} +declare enum ResponseType { "basic", "cors", "default", "error", "opaque" } + +declare type HeaderInit = Headers | Array; +declare type BodyInit = ArrayBuffer | ArrayBufferView | Blob | FormData | string; +declare type RequestInfo = Request | string; + +interface RequestInit { + method?: string; + headers?: HeaderInit | { [index: string]: string }; + body?: BodyInit; + mode?: string | RequestMode; + credentials?: string | RequestCredentials; + cache?: string | RequestCache; +} + +interface IHeaders { + get(name: string): string; + getAll(name: string): Array; + has(name: string): boolean; +} + +declare class Headers implements IHeaders { + append(name: string, value: string): void; + delete(name: string):void; + get(name: string): string; + getAll(name: string): Array; + has(name: string): boolean; + set(name: string, value: string): void; +} + +interface IBody { + bodyUsed: boolean; + arrayBuffer(): Promise; + blob(): Promise; + formData(): Promise; + json(): Promise; + json(): Promise; + text(): Promise; +} + +declare class Body implements IBody { + bodyUsed: boolean; + arrayBuffer(): Promise; + blob(): Promise; + formData(): Promise; + json(): Promise; + json(): Promise; + text(): Promise; +} + +interface IRequest extends IBody { + method: string; + url: string; + headers: Headers; + context: string | RequestContext; + referrer: string; + mode: string | RequestMode; + credentials: string | RequestCredentials; + cache: string | RequestCache; +} + +declare class Request extends Body implements IRequest { + constructor(input: string | Request, init?: RequestInit); + method: string; + url: string; + headers: Headers; + context: string | RequestContext; + referrer: string; + mode: string | RequestMode; + credentials: string | RequestCredentials; + cache: string | RequestCache; +} + +interface IResponse extends IBody { + url: string; + status: number; + statusText: string; + ok: boolean; + headers: IHeaders; + type: string | ResponseType; + size: number; + timeout: number; + redirect(url: string, status: number): IResponse; + error(): IResponse; + clone(): IResponse; +} + +interface IFetchStatic { + Promise: any; + Headers: IHeaders + Request: IRequest; + Response: IResponse; + (url: string | IRequest, init?: RequestInit): Promise; +} + +declare var fetch: IFetchStatic; + +declare module "isomorphic-fetch" { + export = fetch; +} diff --git a/templates/ReactSpa/typings/tsd.d.ts b/templates/ReactSpa/typings/tsd.d.ts index f54e1c6..6491ac9 100644 --- a/templates/ReactSpa/typings/tsd.d.ts +++ b/templates/ReactSpa/typings/tsd.d.ts @@ -3,4 +3,4 @@ /// /// /// -/// +/// diff --git a/templates/ReactSpa/typings/whatwg-fetch/whatwg-fetch.d.ts b/templates/ReactSpa/typings/whatwg-fetch/whatwg-fetch.d.ts deleted file mode 100644 index 64dd904..0000000 --- a/templates/ReactSpa/typings/whatwg-fetch/whatwg-fetch.d.ts +++ /dev/null @@ -1,85 +0,0 @@ -// Type definitions for fetch API -// Project: https://github.com/github/fetch -// Definitions by: Ryan Graham -// Definitions: https://github.com/borisyankov/DefinitelyTyped - -declare class Request extends Body { - constructor(input: string|Request, init?:RequestInit); - method: string; - url: string; - headers: Headers; - context: string|RequestContext; - referrer: string; - mode: string|RequestMode; - credentials: string|RequestCredentials; - cache: string|RequestCache; -} - -interface RequestInit { - method?: string; - headers?: HeaderInit|{ [index: string]: string }; - body?: BodyInit; - mode?: string|RequestMode; - credentials?: string|RequestCredentials; - cache?: string|RequestCache; -} - -declare enum RequestContext { - "audio", "beacon", "cspreport", "download", "embed", "eventsource", "favicon", "fetch", - "font", "form", "frame", "hyperlink", "iframe", "image", "imageset", "import", - "internal", "location", "manifest", "object", "ping", "plugin", "prefetch", "script", - "serviceworker", "sharedworker", "subresource", "style", "track", "video", "worker", - "xmlhttprequest", "xslt" -} -declare enum RequestMode { "same-origin", "no-cors", "cors" } -declare enum RequestCredentials { "omit", "same-origin", "include" } -declare enum RequestCache { "default", "no-store", "reload", "no-cache", "force-cache", "only-if-cached" } - -declare class Headers { - append(name: string, value: string): void; - delete(name: string):void; - get(name: string): string; - getAll(name: string): Array; - has(name: string): boolean; - set(name: string, value: string): void; -} - -declare class Body { - bodyUsed: boolean; - arrayBuffer(): Promise; - blob(): Promise; - formData(): Promise; - json(): Promise; - json(): Promise; - text(): Promise; -} -declare class Response extends Body { - constructor(body?: BodyInit, init?: ResponseInit); - error(): Response; - redirect(url: string, status: number): Response; - type: string|ResponseType; - url: string; - status: number; - ok: boolean; - statusText: string; - headers: Headers; - clone(): Response; -} - -declare enum ResponseType { "basic", "cors", "default", "error", "opaque" } - -interface ResponseInit { - status: number; - statusText?: string; - headers?: HeaderInit; -} - -declare type HeaderInit = Headers|Array; -declare type BodyInit = Blob|FormData|string; -declare type RequestInfo = Request|string; - -interface Window { - fetch(url: string|Request, init?: RequestInit): Promise; -} - -declare var fetch: typeof window.fetch; diff --git a/templates/ReactSpa/webpack.config.vendor.js b/templates/ReactSpa/webpack.config.vendor.js index 814f23d..ac56c0c 100644 --- a/templates/ReactSpa/webpack.config.vendor.js +++ b/templates/ReactSpa/webpack.config.vendor.js @@ -15,7 +15,7 @@ module.exports = { ] }, entry: { - vendor: ['bootstrap', 'bootstrap/dist/css/bootstrap.css', 'react', 'react-dom', 'react-router', 'style-loader', 'jquery'], + vendor: ['bootstrap', 'bootstrap/dist/css/bootstrap.css', 'isomorphic-fetch', 'react', 'react-dom', 'react-router', 'style-loader', 'jquery'], }, output: { path: path.join(__dirname, 'wwwroot', 'dist'), From eed4d8c2119fc75aea95b7b45bb2cbefa0e5277a Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Fri, 19 Aug 2016 16:38:39 -0700 Subject: [PATCH 17/55] Child Node processes poll and exit when parent has exited. Fixes #270 --- .../Content/Node/entrypoint-http.js | 69 ++++++++++++ .../Content/Node/entrypoint-socket.js | 103 +++++++++++++++--- .../HostingModels/OutOfProcessNodeInstance.cs | 3 +- .../TypeScript/HttpNodeInstanceEntryPoint.ts | 3 + .../SocketNodeInstanceEntryPoint.ts | 3 + .../TypeScript/Util/ExitWhenParentExits.ts | 62 +++++++++++ 6 files changed, 225 insertions(+), 18 deletions(-) create mode 100644 src/Microsoft.AspNetCore.NodeServices/TypeScript/Util/ExitWhenParentExits.ts diff --git a/src/Microsoft.AspNetCore.NodeServices/Content/Node/entrypoint-http.js b/src/Microsoft.AspNetCore.NodeServices/Content/Node/entrypoint-http.js index fef873e..76a00ef 100644 --- a/src/Microsoft.AspNetCore.NodeServices/Content/Node/entrypoint-http.js +++ b/src/Microsoft.AspNetCore.NodeServices/Content/Node/entrypoint-http.js @@ -58,6 +58,7 @@ var http = __webpack_require__(3); var path = __webpack_require__(4); var ArgsUtil_1 = __webpack_require__(5); + var ExitWhenParentExits_1 = __webpack_require__(6); // Webpack doesn't support dynamic requires for files not present at compile time, so grab a direct // reference to Node's runtime 'require' function. var dynamicRequire = eval('require'); @@ -121,6 +122,7 @@ // Signal to the NodeServices base class that we're ready to accept invocations console.log('[Microsoft.AspNetCore.NodeServices:Listening]'); }); + ExitWhenParentExits_1.exitWhenParentExits(parseInt(parsedArgs.parentPid)); function readRequestBodyAsJson(request, callback) { var requestBodyAsString = ''; request @@ -208,5 +210,72 @@ exports.parseArgs = parseArgs; +/***/ }, +/* 6 */ +/***/ function(module, exports) { + + /* + In general, we want the Node child processes to be terminated as soon as the parent .NET processes exit, + because we have no further use for them. If the .NET process shuts down gracefully, it will run its + finalizers, one of which (in OutOfProcessNodeInstance.cs) will kill its associated Node process immediately. + + But if the .NET process is terminated forcefully (e.g., on Linux/OSX with 'kill -9'), then it won't have + any opportunity to shut down its child processes, and by default they will keep running. In this case, it's + up to the child process to detect this has happened and terminate itself. + + There are many possible approaches to detecting when a parent process has exited, most of which behave + differently between Windows and Linux/OS X: + + - On Windows, the parent process can mark its child as being a 'job' that should auto-terminate when + the parent does (http://stackoverflow.com/a/4657392). Not cross-platform. + - The child Node process can get a callback when the parent disconnects (process.on('disconnect', ...)). + But despite http://stackoverflow.com/a/16487966, no callback fires in any case I've tested (Windows / OS X). + - The child Node process can get a callback when its stdin/stdout are disconnected, as described at + http://stackoverflow.com/a/15693934. This works well on OS X, but calling stdout.resume() on Windows + causes the process to terminate prematurely. + - I don't know why, but on Windows, it's enough to invoke process.stdin.resume(). For some reason this causes + the child Node process to exit as soon as the parent one does, but I don't see this documented anywhere. + - You can poll to see if the parent process, or your stdin/stdout connection to it, is gone + - You can directly pass a parent process PID to the child, and then have the child poll to see if it's + still running (e.g., using process.kill(pid, 0), which doesn't kill it but just tests whether it exists, + as per https://nodejs.org/api/process.html#process_process_kill_pid_signal) + - Or, on each poll, you can try writing to process.stdout. If the parent has died, then this will throw. + However I don't see this documented anywhere. It would be nice if you could just poll for whether or not + process.stdout is still connected (without actually writing to it) but I haven't found any property whose + value changes until you actually try to write to it. + + Of these, the only cross-platform approach that is actually documented as a valid strategy is simply polling + to check whether the parent PID is still running. So that's what we do here. + */ + "use strict"; + var pollIntervalMs = 1000; + function exitWhenParentExits(parentPid) { + setInterval(function () { + if (!processExists(parentPid)) { + // Can't log anything at this point, because out stdout was connected to the parent, + // but the parent is gone. + process.exit(); + } + }, pollIntervalMs); + } + exports.exitWhenParentExits = exitWhenParentExits; + function processExists(pid) { + try { + // Sending signal 0 - on all platforms - tests whether the process exists. As long as it doesn't + // throw, that means it does exist. + process.kill(pid, 0); + return true; + } + catch (ex) { + // If the reason for the error is that we don't have permission to ask about this process, + // report that as a separate problem. + if (ex.code === 'EPERM') { + throw new Error("Attempted to check whether process " + pid + " was running, but got a permissions error."); + } + return false; + } + } + + /***/ } /******/ ]))); \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.NodeServices/Content/Node/entrypoint-socket.js b/src/Microsoft.AspNetCore.NodeServices/Content/Node/entrypoint-socket.js index e59b6ed..c32a882 100644 --- a/src/Microsoft.AspNetCore.NodeServices/Content/Node/entrypoint-socket.js +++ b/src/Microsoft.AspNetCore.NodeServices/Content/Node/entrypoint-socket.js @@ -44,7 +44,7 @@ /* 0 */ /***/ function(module, exports, __webpack_require__) { - module.exports = __webpack_require__(6); + module.exports = __webpack_require__(7); /***/ }, @@ -124,17 +124,85 @@ /***/ }, /* 6 */ +/***/ function(module, exports) { + + /* + In general, we want the Node child processes to be terminated as soon as the parent .NET processes exit, + because we have no further use for them. If the .NET process shuts down gracefully, it will run its + finalizers, one of which (in OutOfProcessNodeInstance.cs) will kill its associated Node process immediately. + + But if the .NET process is terminated forcefully (e.g., on Linux/OSX with 'kill -9'), then it won't have + any opportunity to shut down its child processes, and by default they will keep running. In this case, it's + up to the child process to detect this has happened and terminate itself. + + There are many possible approaches to detecting when a parent process has exited, most of which behave + differently between Windows and Linux/OS X: + + - On Windows, the parent process can mark its child as being a 'job' that should auto-terminate when + the parent does (http://stackoverflow.com/a/4657392). Not cross-platform. + - The child Node process can get a callback when the parent disconnects (process.on('disconnect', ...)). + But despite http://stackoverflow.com/a/16487966, no callback fires in any case I've tested (Windows / OS X). + - The child Node process can get a callback when its stdin/stdout are disconnected, as described at + http://stackoverflow.com/a/15693934. This works well on OS X, but calling stdout.resume() on Windows + causes the process to terminate prematurely. + - I don't know why, but on Windows, it's enough to invoke process.stdin.resume(). For some reason this causes + the child Node process to exit as soon as the parent one does, but I don't see this documented anywhere. + - You can poll to see if the parent process, or your stdin/stdout connection to it, is gone + - You can directly pass a parent process PID to the child, and then have the child poll to see if it's + still running (e.g., using process.kill(pid, 0), which doesn't kill it but just tests whether it exists, + as per https://nodejs.org/api/process.html#process_process_kill_pid_signal) + - Or, on each poll, you can try writing to process.stdout. If the parent has died, then this will throw. + However I don't see this documented anywhere. It would be nice if you could just poll for whether or not + process.stdout is still connected (without actually writing to it) but I haven't found any property whose + value changes until you actually try to write to it. + + Of these, the only cross-platform approach that is actually documented as a valid strategy is simply polling + to check whether the parent PID is still running. So that's what we do here. + */ + "use strict"; + var pollIntervalMs = 1000; + function exitWhenParentExits(parentPid) { + setInterval(function () { + if (!processExists(parentPid)) { + // Can't log anything at this point, because out stdout was connected to the parent, + // but the parent is gone. + process.exit(); + } + }, pollIntervalMs); + } + exports.exitWhenParentExits = exitWhenParentExits; + function processExists(pid) { + try { + // Sending signal 0 - on all platforms - tests whether the process exists. As long as it doesn't + // throw, that means it does exist. + process.kill(pid, 0); + return true; + } + catch (ex) { + // If the reason for the error is that we don't have permission to ask about this process, + // report that as a separate problem. + if (ex.code === 'EPERM') { + throw new Error("Attempted to check whether process " + pid + " was running, but got a permissions error."); + } + return false; + } + } + + +/***/ }, +/* 7 */ /***/ function(module, exports, __webpack_require__) { "use strict"; // Limit dependencies to core Node modules. This means the code in this file has to be very low-level and unattractive, // but simplifies things for the consumer of this module. __webpack_require__(2); - var net = __webpack_require__(7); + var net = __webpack_require__(8); var path = __webpack_require__(4); - var readline = __webpack_require__(8); + var readline = __webpack_require__(9); var ArgsUtil_1 = __webpack_require__(5); - var virtualConnectionServer = __webpack_require__(9); + var ExitWhenParentExits_1 = __webpack_require__(6); + var virtualConnectionServer = __webpack_require__(10); // Webpack doesn't support dynamic requires for files not present at compile time, so grab a direct // reference to Node's runtime 'require' function. var dynamicRequire = eval('require'); @@ -189,27 +257,28 @@ var parsedArgs = ArgsUtil_1.parseArgs(process.argv); var listenAddress = (useWindowsNamedPipes ? '\\\\.\\pipe\\' : '/tmp/') + parsedArgs.listenAddress; server.listen(listenAddress); + ExitWhenParentExits_1.exitWhenParentExits(parseInt(parsedArgs.parentPid)); -/***/ }, -/* 7 */ -/***/ function(module, exports) { - - module.exports = require("net"); - /***/ }, /* 8 */ /***/ function(module, exports) { - module.exports = require("readline"); + module.exports = require("net"); /***/ }, /* 9 */ +/***/ function(module, exports) { + + module.exports = require("readline"); + +/***/ }, +/* 10 */ /***/ function(module, exports, __webpack_require__) { "use strict"; - var events_1 = __webpack_require__(10); - var VirtualConnection_1 = __webpack_require__(11); + var events_1 = __webpack_require__(11); + var VirtualConnection_1 = __webpack_require__(12); // Keep this in sync with the equivalent constant in the .NET code. Both sides split up their transmissions into frames with this max length, // and both will reject longer frames. var MaxFrameBodyLength = 16 * 1024; @@ -390,13 +459,13 @@ /***/ }, -/* 10 */ +/* 11 */ /***/ function(module, exports) { module.exports = require("events"); /***/ }, -/* 11 */ +/* 12 */ /***/ function(module, exports, __webpack_require__) { "use strict"; @@ -405,7 +474,7 @@ function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; - var stream_1 = __webpack_require__(12); + var stream_1 = __webpack_require__(13); /** * Represents a virtual connection. Multiple virtual connections may be multiplexed over a single physical socket connection. */ @@ -446,7 +515,7 @@ /***/ }, -/* 12 */ +/* 13 */ /***/ function(module, exports) { module.exports = require("stream"); diff --git a/src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs b/src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs index d9ede58..73dcedc 100644 --- a/src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs +++ b/src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs @@ -114,9 +114,10 @@ If you haven't yet installed node-inspector, you can do so as follows: debuggingArgs = string.Empty; } + var thisProcessPid = Process.GetCurrentProcess().Id; var startInfo = new ProcessStartInfo("node") { - Arguments = debuggingArgs + "\"" + entryPointFilename + "\" " + (commandLineArguments ?? string.Empty), + Arguments = $"{debuggingArgs}\"{entryPointFilename}\" --parentPid {thisProcessPid} {commandLineArguments ?? string.Empty}", UseShellExecute = false, RedirectStandardInput = true, RedirectStandardOutput = true, diff --git a/src/Microsoft.AspNetCore.NodeServices/TypeScript/HttpNodeInstanceEntryPoint.ts b/src/Microsoft.AspNetCore.NodeServices/TypeScript/HttpNodeInstanceEntryPoint.ts index 0e10d99..431810e 100644 --- a/src/Microsoft.AspNetCore.NodeServices/TypeScript/HttpNodeInstanceEntryPoint.ts +++ b/src/Microsoft.AspNetCore.NodeServices/TypeScript/HttpNodeInstanceEntryPoint.ts @@ -4,6 +4,7 @@ import './Util/OverrideStdOutputs'; import * as http from 'http'; import * as path from 'path'; import { parseArgs } from './Util/ArgsUtil'; +import { exitWhenParentExits } from './Util/ExitWhenParentExits'; // Webpack doesn't support dynamic requires for files not present at compile time, so grab a direct // reference to Node's runtime 'require' function. @@ -73,6 +74,8 @@ server.listen(requestedPortOrZero, 'localhost', function () { console.log('[Microsoft.AspNetCore.NodeServices:Listening]'); }); +exitWhenParentExits(parseInt(parsedArgs.parentPid)); + function readRequestBodyAsJson(request, callback) { let requestBodyAsString = ''; request diff --git a/src/Microsoft.AspNetCore.NodeServices/TypeScript/SocketNodeInstanceEntryPoint.ts b/src/Microsoft.AspNetCore.NodeServices/TypeScript/SocketNodeInstanceEntryPoint.ts index 0a6f713..acb5df1 100644 --- a/src/Microsoft.AspNetCore.NodeServices/TypeScript/SocketNodeInstanceEntryPoint.ts +++ b/src/Microsoft.AspNetCore.NodeServices/TypeScript/SocketNodeInstanceEntryPoint.ts @@ -6,6 +6,7 @@ import * as path from 'path'; import * as readline from 'readline'; import { Duplex } from 'stream'; import { parseArgs } from './Util/ArgsUtil'; +import { exitWhenParentExits } from './Util/ExitWhenParentExits'; import * as virtualConnectionServer from './VirtualConnections/VirtualConnectionServer'; // Webpack doesn't support dynamic requires for files not present at compile time, so grab a direct @@ -69,6 +70,8 @@ const parsedArgs = parseArgs(process.argv); const listenAddress = (useWindowsNamedPipes ? '\\\\.\\pipe\\' : '/tmp/') + parsedArgs.listenAddress; server.listen(listenAddress); +exitWhenParentExits(parseInt(parsedArgs.parentPid)); + interface RpcInvocation { moduleName: string; exportedFunctionName: string; diff --git a/src/Microsoft.AspNetCore.NodeServices/TypeScript/Util/ExitWhenParentExits.ts b/src/Microsoft.AspNetCore.NodeServices/TypeScript/Util/ExitWhenParentExits.ts new file mode 100644 index 0000000..672e40b --- /dev/null +++ b/src/Microsoft.AspNetCore.NodeServices/TypeScript/Util/ExitWhenParentExits.ts @@ -0,0 +1,62 @@ +/* +In general, we want the Node child processes to be terminated as soon as the parent .NET processes exit, +because we have no further use for them. If the .NET process shuts down gracefully, it will run its +finalizers, one of which (in OutOfProcessNodeInstance.cs) will kill its associated Node process immediately. + +But if the .NET process is terminated forcefully (e.g., on Linux/OSX with 'kill -9'), then it won't have +any opportunity to shut down its child processes, and by default they will keep running. In this case, it's +up to the child process to detect this has happened and terminate itself. + +There are many possible approaches to detecting when a parent process has exited, most of which behave +differently between Windows and Linux/OS X: + + - On Windows, the parent process can mark its child as being a 'job' that should auto-terminate when + the parent does (http://stackoverflow.com/a/4657392). Not cross-platform. + - The child Node process can get a callback when the parent disconnects (process.on('disconnect', ...)). + But despite http://stackoverflow.com/a/16487966, no callback fires in any case I've tested (Windows / OS X). + - The child Node process can get a callback when its stdin/stdout are disconnected, as described at + http://stackoverflow.com/a/15693934. This works well on OS X, but calling stdout.resume() on Windows + causes the process to terminate prematurely. + - I don't know why, but on Windows, it's enough to invoke process.stdin.resume(). For some reason this causes + the child Node process to exit as soon as the parent one does, but I don't see this documented anywhere. + - You can poll to see if the parent process, or your stdin/stdout connection to it, is gone + - You can directly pass a parent process PID to the child, and then have the child poll to see if it's + still running (e.g., using process.kill(pid, 0), which doesn't kill it but just tests whether it exists, + as per https://nodejs.org/api/process.html#process_process_kill_pid_signal) + - Or, on each poll, you can try writing to process.stdout. If the parent has died, then this will throw. + However I don't see this documented anywhere. It would be nice if you could just poll for whether or not + process.stdout is still connected (without actually writing to it) but I haven't found any property whose + value changes until you actually try to write to it. + +Of these, the only cross-platform approach that is actually documented as a valid strategy is simply polling +to check whether the parent PID is still running. So that's what we do here. +*/ + +const pollIntervalMs = 1000; + +export function exitWhenParentExits(parentPid: number) { + setInterval(() => { + if (!processExists(parentPid)) { + // Can't log anything at this point, because out stdout was connected to the parent, + // but the parent is gone. + process.exit(); + } + }, pollIntervalMs); +} + +function processExists(pid: number) { + try { + // Sending signal 0 - on all platforms - tests whether the process exists. As long as it doesn't + // throw, that means it does exist. + process.kill(pid, 0); + return true; + } catch (ex) { + // If the reason for the error is that we don't have permission to ask about this process, + // report that as a separate problem. + if (ex.code === 'EPERM') { + throw new Error(`Attempted to check whether process ${pid} was running, but got a permissions error.`); + } + + return false; + } +} From 7f5810a62221429878664e3a7cbae674531c6116 Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Thu, 1 Sep 2016 12:36:47 +0100 Subject: [PATCH 18/55] Add docs about configuring Webpack to build LESS/SASS --- .../README.md | 134 +++++++++++++++++- 1 file changed, 133 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.AspNetCore.SpaServices/README.md b/src/Microsoft.AspNetCore.SpaServices/README.md index 59e05fd..4cfb961 100644 --- a/src/Microsoft.AspNetCore.SpaServices/README.md +++ b/src/Microsoft.AspNetCore.SpaServices/README.md @@ -325,7 +325,7 @@ Benefits: It lets you work as if the browser natively understands whatever file types you are working with (e.g., TypeScript, SASS), because it's as if there's no build process to wait for. -### Example: A simple Webpack setup +### Example: A simple Webpack setup that builds TypeScript **Note:** If you already have Webpack in your project, then you can skip this section. @@ -376,6 +376,138 @@ The Webpack loader, `ts-loader`, follows all chains of reference from `MyApp.ts` So that's enough to build TypeScript. Here's where webpack dev middleware comes in to auto-build your code whenever needed (so you don't need any file watchers or to run `webpack` manually), and optionally hot module replacement (HMR) to push your changes automatically from code editor to browser without even reloading the page. +### Example: A simple Webpack setup that builds LESS + +Following on from the preceding example that builds TypeScript, you could extend your Webpack configuration further to support building LESS. There are two major approaches to doing this: + +1. **Have each build embed the style information into your JavaScript code**. At runtime, Webpack can dynamically attach the styles to your document via JavaScript. This has certain benefits during development. + +2. **Or, have each build write a standalone `.css` file to disk**. At runtime, load it using a regular `` tag. This is likely to be the approach you'll want for production use as it's the most robust and best-performing option. + +If instead of LESS you prefer SASS or another CSS preprocessor, the exact same techniques should work, but of course you'll need to replace the `less-loader` with an equivalent Webpack loader for SASS or your chosen preprocessor. + +#### Approach 1: Loading the styles using JavaScript + +This technique is a little simpler to set up than technique 1, plus it works flawlessly with Hot Module Replacement (HMR). The downside is that it's really only good for development time, because in production you probably don't want users to wait until JavaScript is loaded before styles are applied to the page (this would mean they'd see a 'flash of unstyled content' while the page is being loaded). + +First create a `.less` file in your project. For example, create a file at `ClientApp/styles/mystyles.less` containing: + +```less +@base: #f938ab; + +h1 { + color: @base; +} +``` + +Reference this file from an `import` or `require` statement in one of your JavaScript or TypeScript files. For example, if you've got a `boot-client.ts` file, add the following near the top: + +```javascript +import './styles/mystyles.less'; +``` + +If you try to run the Webpack compiler now (e.g., via `webpack` on the command line), you'll get an error saying it doesn't know how to build `.less` files. So, it's time to install a Webpack loader for LESS (plus related NPM modules). In a command prompt at your project's root directory, run: + +``` +npm install --save less-loader less +``` + +Finally, tell Webpack to use this whenever it encounters a `.less` file. In `webpack.config.js`, add to the `loaders` array: + +``` +{ test: /\.less/, loader: 'style!css!less' } +``` + +This means that when you `import` or `require` a `.less` file, it should pass it first to the LESS compiler to produce CSS, then the output goes to the CSS and Style loaders that know how to attach it dynamically to the page at runtime. + +That's all you need to do! Restart your site and you should see the LESS styles being applied. This technique is compatible with both source maps and Hot Module Replacement (HMR), so you can edit your `.less` files at will and see the changes appearing live in the browser. + +**Scoping styles in Angular 2 components** + +If you're using Angular 2, you can define styles on a per-component basis rather than just globally for your whole app. Angular then takes care of ensuring that only the intended styles are applied to each component, even if the selector names would otherwise clash. To extend the above technique to per-component styling, first install the `to-string-loader` NPM module: + +``` +npm install --save to-string-loader +``` + +Then in your `webpack.config.js`, simplify the `loader` entry for LESS files so that it just outputs `css` (without preparing it for use in a `style` tag): + +```javascript +{ test: /\.less/, loader: 'css!less' } +``` + +Now **you must remove any direct global references to the `.less` file**, since you'll no longer be loading it globally. So if you previously loaded `mystyles.less` using an `import` or `require` statement in `boot-client.ts` or similar, remove that line. + +Finally, load the LESS file scoped to a particular Angular 2 component by declaring a `styles` value for that component. For example, + +```javascript +@ng.Component({ + selector: ... leave value unchanged ..., + template: ... leave value unchanged ..., + styles: [require('to-string!../../styles/mystyles.less')] +}) +export class YourComponent { + ... code remains here ... +} +``` + +Now when you reload your page, you should file that the styles in `mystyles.less` are applied, but only to the component where you attached it. It's reasonable to use this technique in production because, even though the styles now depend on JavaScript to be applied, they are only used on elements that are injected via JavaScript anyway. + +#### Approach 2: Building LESS to CSS files on disk + +This technique takes a little more work to set up than technique 1, and lacks compatibility with HMR. But it's much better for production use if your styles are applied to the whole page (not just elements constructed via JavaScript), because it loads the CSS independently of JavaScript. + +First add a `.less` file into your project. For example, create a file at `ClientApp/styles/mystyles.less` containing: + +```less +@base: #f938ab; + +h1 { + color: @base; +} +``` + +Reference this file from an `import` or `require` statement in one of your JavaScript or TypeScript files. For example, if you've got a `boot-client.ts` file, add the following near the top: + +```javascript +import './styles/mystyles.less'; +``` + +If you try to run the Webpack compiler now (e.g., via `webpack` on the command line), you'll get an error saying it doesn't know how to build `.less` files. So, it's time to install a Webpack loader for LESS (plus related NPM modules). In a command prompt at your project's root directory, run: + +``` +npm install --save less less-loader extract-text-webpack-plugin +``` + +Next, you can extend your Webpack configuration to handle `.less` files. In `webpack.config.js`, at the top, add: + +```javascript +var extractStyles = new (require('extract-text-webpack-plugin'))('mystyles.css'); +``` + +This creates a plugin instance that will output text to a file called `mystyles.css`. You can now compile `.less` files and emit the resulting CSS text into that file. To do so, add the following to the `loaders` array in your Webpack configuration: + +```javascript +{ test: /\.less$/, loader: extractStyles.extract('css!less') } +``` + +This tells Webpack that, whenever it finds a `.less` file, it should use the LESS loader to produce CSS, and then feed that CSS into the `extractStyles` object which you've already configured to write a file on disk called `mystyles.css`. Finally, for this to actually work, you need to include `extractStyles` in the list of active plugins. Just add that object to the `plugins` array in your Webpack config, e.g.: + +```javascript +plugins: [ + extractStyles, + ... leave any other plugins here ... +] +``` + +If you run `webpack` on the command line now, you should now find that it emits a new file at `dist/mystyles.css`. You can make browsers load this file simply by adding a regular `` tag. For example, in `Views/Shared/_Layout.cshtml`, add: + +```html + +``` + +**Note** This technique (writing the built `.css` file to disk) is ideal for production use. But note that, at development time, *it does not support Hot Module Replacement (HMR)*. You will need to reload the page each time you edit your `.less` file. This is a known limitation of `extract-text-webpack-plugin`. If you have constructive opinions on how this can be improved, see the [discussion here](https://github.com/webpack/extract-text-webpack-plugin/issues/30). + ### Enabling webpack dev middleware First install the `Microsoft.AspNetCore.SpaServices` NuGet package and the `aspnet-webpack` NPM package, then go to your `Startup.cs` file, and **before your call to `UseStaticFiles`**, add the following: From 4effd630a4ae1763fc7820e2b9a14665e16c4c78 Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Thu, 1 Sep 2016 12:42:12 +0100 Subject: [PATCH 19/55] Fix typos --- src/Microsoft.AspNetCore.SpaServices/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.AspNetCore.SpaServices/README.md b/src/Microsoft.AspNetCore.SpaServices/README.md index 4cfb961..fda8baf 100644 --- a/src/Microsoft.AspNetCore.SpaServices/README.md +++ b/src/Microsoft.AspNetCore.SpaServices/README.md @@ -388,7 +388,7 @@ If instead of LESS you prefer SASS or another CSS preprocessor, the exact same t #### Approach 1: Loading the styles using JavaScript -This technique is a little simpler to set up than technique 1, plus it works flawlessly with Hot Module Replacement (HMR). The downside is that it's really only good for development time, because in production you probably don't want users to wait until JavaScript is loaded before styles are applied to the page (this would mean they'd see a 'flash of unstyled content' while the page is being loaded). +This technique is a little simpler to set up than technique 2, plus it works flawlessly with Hot Module Replacement (HMR). The downside is that it's really only good for development time, because in production you probably don't want users to wait until JavaScript is loaded before styles are applied to the page (this would mean they'd see a 'flash of unstyled content' while the page is being loaded). First create a `.less` file in your project. For example, create a file at `ClientApp/styles/mystyles.less` containing: @@ -506,7 +506,7 @@ If you run `webpack` on the command line now, you should now find that it emits ``` -**Note** This technique (writing the built `.css` file to disk) is ideal for production use. But note that, at development time, *it does not support Hot Module Replacement (HMR)*. You will need to reload the page each time you edit your `.less` file. This is a known limitation of `extract-text-webpack-plugin`. If you have constructive opinions on how this can be improved, see the [discussion here](https://github.com/webpack/extract-text-webpack-plugin/issues/30). +**Note:** This technique (writing the built `.css` file to disk) is ideal for production use. But note that, at development time, *it does not support Hot Module Replacement (HMR)*. You will need to reload the page each time you edit your `.less` file. This is a known limitation of `extract-text-webpack-plugin`. If you have constructive opinions on how this can be improved, see the [discussion here](https://github.com/webpack/extract-text-webpack-plugin/issues/30). ### Enabling webpack dev middleware From 09e1cd3b077aa505be73fdcd2f87658510e8cc34 Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Thu, 1 Sep 2016 13:59:28 +0100 Subject: [PATCH 20/55] TS compiler should ignore "bin" dir because, after VS publish, it might contain duplicates of the app's source files. Fixes #279. --- templates/Angular2Spa/tsconfig.json | 1 + templates/KnockoutSpa/tsconfig.json | 1 + templates/ReactReduxSpa/tsconfig.json | 1 + templates/ReactSpa/tsconfig.json | 1 + templates/WebApplicationBasic/tsconfig.json | 1 + 5 files changed, 5 insertions(+) diff --git a/templates/Angular2Spa/tsconfig.json b/templates/Angular2Spa/tsconfig.json index ef5deed..e141587 100644 --- a/templates/Angular2Spa/tsconfig.json +++ b/templates/Angular2Spa/tsconfig.json @@ -8,6 +8,7 @@ "skipDefaultLibCheck": true }, "exclude": [ + "bin", "node_modules" ] } diff --git a/templates/KnockoutSpa/tsconfig.json b/templates/KnockoutSpa/tsconfig.json index 61872e1..1404455 100644 --- a/templates/KnockoutSpa/tsconfig.json +++ b/templates/KnockoutSpa/tsconfig.json @@ -6,6 +6,7 @@ "skipDefaultLibCheck": true }, "exclude": [ + "bin", "node_modules" ] } diff --git a/templates/ReactReduxSpa/tsconfig.json b/templates/ReactReduxSpa/tsconfig.json index b6603ac..c5ae211 100644 --- a/templates/ReactReduxSpa/tsconfig.json +++ b/templates/ReactReduxSpa/tsconfig.json @@ -8,6 +8,7 @@ "skipDefaultLibCheck": true }, "exclude": [ + "bin", "node_modules" ] } diff --git a/templates/ReactSpa/tsconfig.json b/templates/ReactSpa/tsconfig.json index 39a15ea..69ed93a 100644 --- a/templates/ReactSpa/tsconfig.json +++ b/templates/ReactSpa/tsconfig.json @@ -7,6 +7,7 @@ "skipDefaultLibCheck": true }, "exclude": [ + "bin", "node_modules" ] } diff --git a/templates/WebApplicationBasic/tsconfig.json b/templates/WebApplicationBasic/tsconfig.json index 61872e1..1404455 100644 --- a/templates/WebApplicationBasic/tsconfig.json +++ b/templates/WebApplicationBasic/tsconfig.json @@ -6,6 +6,7 @@ "skipDefaultLibCheck": true }, "exclude": [ + "bin", "node_modules" ] } From 61fd9009749a6c8b55f375209b40898a5db6b2b0 Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Thu, 1 Sep 2016 14:10:42 +0100 Subject: [PATCH 21/55] Add missing setup step to docs. Fixes #290. --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 85dd692..9322418 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,8 @@ Also in this repo, [the `samples` directory](https://github.com/aspnet/JavaScrip * Clone this repo * Change directory to the same you want to run (e.g., `cd samples/angular/MusicStore`) - * Restore dependencies (run `dotnet restore` and `npm install`) + * Restore dependencies (run `dotnet restore` and `npm install`). + * If you're trying to run the Angular 2 "Music Store" sample, then also run `gulp` (which you need to have installed globally). None of the other samples require this. * Run the application (`dotnet run`) * Browse to [http://localhost:5000](http://localhost:5000) From f04fb8c42115d3816f9b8005dacefe7e6e485325 Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Thu, 1 Sep 2016 15:51:53 +0100 Subject: [PATCH 22/55] Design review: Always instantiate via DI --- samples/misc/LatencyTest/Program.cs | 21 +++-- samples/misc/LatencyTest/project.json | 3 +- samples/misc/Webpack/webpack.config.js | 2 +- .../Configuration/Configuration.cs | 89 ++++++++++--------- .../Configuration/NodeServicesOptions.cs | 16 ---- .../NodeServicesImpl.cs | 4 +- .../README.md | 30 +++++-- .../Prerendering/PrerenderTagHelper.cs | 7 +- .../Webpack/WebpackDevMiddleware.cs | 16 ++-- 9 files changed, 97 insertions(+), 91 deletions(-) diff --git a/samples/misc/LatencyTest/Program.cs b/samples/misc/LatencyTest/Program.cs index 2087d29..9dc3ce8 100755 --- a/samples/misc/LatencyTest/Program.cs +++ b/samples/misc/LatencyTest/Program.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.IO; using System.Threading.Tasks; using Microsoft.AspNetCore.NodeServices; +using Microsoft.Extensions.DependencyInjection; namespace ConsoleApplication { @@ -12,7 +13,17 @@ namespace ConsoleApplication public class Program { public static void Main(string[] args) { - using (var nodeServices = CreateNodeServices(NodeServicesOptions.DefaultNodeHostingModel)) { + // Set up the DI system + var services = new ServiceCollection(); + services.AddNodeServices(new NodeServicesOptions { + HostingModel = NodeServicesOptions.DefaultNodeHostingModel, + ProjectPath = Directory.GetCurrentDirectory(), + WatchFileExtensions = new string[] {} // Don't watch anything + }); + var serviceProvider = services.BuildServiceProvider(); + + // Now instantiate an INodeServices and use it + using (var nodeServices = serviceProvider.GetRequiredService()) { MeasureLatency(nodeServices).Wait(); } } @@ -34,13 +45,5 @@ namespace ConsoleApplication Console.WriteLine("\nTotal time: {0:F2} milliseconds", 1000 * elapsedSeconds); Console.WriteLine("\nTime per invocation: {0:F2} milliseconds", 1000 * elapsedSeconds / requestCount); } - - private static INodeServices CreateNodeServices(NodeHostingModel hostingModel) { - return Configuration.CreateNodeServices(new NodeServicesOptions { - HostingModel = hostingModel, - ProjectPath = Directory.GetCurrentDirectory(), - WatchFileExtensions = new string[] {} // Don't watch anything - }); - } } } diff --git a/samples/misc/LatencyTest/project.json b/samples/misc/LatencyTest/project.json index 33820b5..15e349c 100755 --- a/samples/misc/LatencyTest/project.json +++ b/samples/misc/LatencyTest/project.json @@ -8,7 +8,8 @@ "version": "1.0.0", "type": "platform" }, - "Microsoft.AspNetCore.NodeServices": "1.0.0-*" + "Microsoft.AspNetCore.NodeServices": "1.0.0-*", + "Microsoft.Extensions.DependencyInjection": "1.0.0" }, "frameworks": { "netcoreapp1.0": { diff --git a/samples/misc/Webpack/webpack.config.js b/samples/misc/Webpack/webpack.config.js index 5ff186d..011ed5f 100644 --- a/samples/misc/Webpack/webpack.config.js +++ b/samples/misc/Webpack/webpack.config.js @@ -10,7 +10,7 @@ module.exports = merge({ }, module: { loaders: [ - { test: /\.ts(x?)$/, exclude: /node_modules/, loader: 'ts-loader' } + { test: /\.ts(x?)$/, exclude: /node_modules/, loader: 'ts-loader?silent' } ], }, entry: { diff --git a/src/Microsoft.AspNetCore.NodeServices/Configuration/Configuration.cs b/src/Microsoft.AspNetCore.NodeServices/Configuration/Configuration.cs index bd76dc8..4cbba69 100644 --- a/src/Microsoft.AspNetCore.NodeServices/Configuration/Configuration.cs +++ b/src/Microsoft.AspNetCore.NodeServices/Configuration/Configuration.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.NodeServices.HostingModels; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Console; +using System.Collections.Generic; namespace Microsoft.AspNetCore.NodeServices { @@ -16,48 +17,18 @@ namespace Microsoft.AspNetCore.NodeServices public static void AddNodeServices(this IServiceCollection serviceCollection, NodeServicesOptions options) { - serviceCollection.AddSingleton(typeof(INodeServices), serviceProvider => - { - // Since this instance is being created through DI, we can access the IHostingEnvironment - // to populate options.ProjectPath if it wasn't explicitly specified. - var hostEnv = serviceProvider.GetRequiredService(); - if (string.IsNullOrEmpty(options.ProjectPath)) - { - options.ProjectPath = hostEnv.ContentRootPath; - } - - // Similarly, we can determine the 'is development' value from the hosting environment - options.AddDefaultEnvironmentVariables(hostEnv.IsDevelopment()); - - // Likewise, if no logger was specified explicitly, we should use the one from DI. - // If it doesn't provide one, CreateNodeInstance will set up a default. - if (options.NodeInstanceOutputLogger == null) - { - var loggerFactory = serviceProvider.GetService(); - if (loggerFactory != null) - { - options.NodeInstanceOutputLogger = loggerFactory.CreateLogger(LogCategoryName); - } - } - - return new NodeServicesImpl(options, () => CreateNodeInstance(options)); - }); + serviceCollection.AddSingleton( + typeof(INodeServices), + serviceProvider => CreateNodeServices(serviceProvider, options)); } - public static INodeServices CreateNodeServices(NodeServicesOptions options) + public static INodeServices CreateNodeServices(IServiceProvider serviceProvider, NodeServicesOptions options) { - return new NodeServicesImpl(options, () => CreateNodeInstance(options)); + return new NodeServicesImpl(() => CreateNodeInstance(serviceProvider, options)); } - private static INodeInstance CreateNodeInstance(NodeServicesOptions options) + private static INodeInstance CreateNodeInstance(IServiceProvider serviceProvider, NodeServicesOptions options) { - // If you've specified no logger, fall back on a default console logger - var logger = options.NodeInstanceOutputLogger; - if (logger == null) - { - logger = new ConsoleLogger(LogCategoryName, null, false); - } - if (options.NodeInstanceFactory != null) { // If you've explicitly supplied an INodeInstance factory, we'll use that. This is useful for @@ -66,17 +37,51 @@ namespace Microsoft.AspNetCore.NodeServices } else { - // Otherwise we'll construct the type of INodeInstance specified by the HostingModel property, - // which itself has a useful default value. + // Otherwise we'll construct the type of INodeInstance specified by the HostingModel property + // (which itself has a useful default value), plus obtain config information from the DI system. + var projectPath = options.ProjectPath; + var envVars = options.EnvironmentVariables == null + ? new Dictionary() + : new Dictionary(options.EnvironmentVariables); + + var hostEnv = serviceProvider.GetService(); + if (hostEnv != null) + { + // In an ASP.NET environment, we can use the IHostingEnvironment data to auto-populate a few + // things that you'd otherwise have to specify manually + if (string.IsNullOrEmpty(projectPath)) + { + projectPath = hostEnv.ContentRootPath; + } + + // Similarly, we can determine the 'is development' value from the hosting environment + if (!envVars.ContainsKey("NODE_ENV")) + { + // These strings are a de-facto standard in Node + envVars["NODE_ENV"] = hostEnv.IsDevelopment() ? "development" : "production"; + } + } + + // If no logger was specified explicitly, we should use the one from DI. + // If it doesn't provide one, we'll set up a default one. + var logger = options.NodeInstanceOutputLogger; + if (logger == null) + { + var loggerFactory = serviceProvider.GetService(); + logger = loggerFactory != null + ? loggerFactory.CreateLogger(LogCategoryName) + : new ConsoleLogger(LogCategoryName, null, false); + } + switch (options.HostingModel) { case NodeHostingModel.Http: - return new HttpNodeInstance(options.ProjectPath, options.WatchFileExtensions, logger, - options.EnvironmentVariables, options.LaunchWithDebugging, options.DebuggingPort, /* port */ 0); + return new HttpNodeInstance(projectPath, options.WatchFileExtensions, logger, + envVars, options.LaunchWithDebugging, options.DebuggingPort, /* port */ 0); case NodeHostingModel.Socket: var pipeName = "pni-" + Guid.NewGuid().ToString("D"); // Arbitrary non-clashing string - return new SocketNodeInstance(options.ProjectPath, options.WatchFileExtensions, pipeName, logger, - options.EnvironmentVariables, options.LaunchWithDebugging, options.DebuggingPort); + return new SocketNodeInstance(projectPath, options.WatchFileExtensions, pipeName, logger, + envVars, options.LaunchWithDebugging, options.DebuggingPort); default: throw new ArgumentException("Unknown hosting model: " + options.HostingModel); } diff --git a/src/Microsoft.AspNetCore.NodeServices/Configuration/NodeServicesOptions.cs b/src/Microsoft.AspNetCore.NodeServices/Configuration/NodeServicesOptions.cs index 4ff981e..ddc9686 100644 --- a/src/Microsoft.AspNetCore.NodeServices/Configuration/NodeServicesOptions.cs +++ b/src/Microsoft.AspNetCore.NodeServices/Configuration/NodeServicesOptions.cs @@ -25,21 +25,5 @@ namespace Microsoft.AspNetCore.NodeServices public bool LaunchWithDebugging { get; set; } public IDictionary EnvironmentVariables { get; set; } public int? DebuggingPort { get; set; } - - public NodeServicesOptions AddDefaultEnvironmentVariables(bool isDevelopmentMode) - { - if (EnvironmentVariables == null) - { - EnvironmentVariables = new Dictionary(); - } - - if (!EnvironmentVariables.ContainsKey("NODE_ENV")) - { - // These strings are a de-facto standard in Node - EnvironmentVariables["NODE_ENV"] = isDevelopmentMode ? "development" : "production"; - } - - return this; - } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.NodeServices/NodeServicesImpl.cs b/src/Microsoft.AspNetCore.NodeServices/NodeServicesImpl.cs index 8223215..037cc96 100644 --- a/src/Microsoft.AspNetCore.NodeServices/NodeServicesImpl.cs +++ b/src/Microsoft.AspNetCore.NodeServices/NodeServicesImpl.cs @@ -19,15 +19,13 @@ namespace Microsoft.AspNetCore.NodeServices internal class NodeServicesImpl : INodeServices { private static TimeSpan ConnectionDrainingTimespan = TimeSpan.FromSeconds(15); - private NodeServicesOptions _options; private Func _nodeInstanceFactory; private INodeInstance _currentNodeInstance; private object _currentNodeInstanceAccessLock = new object(); private Exception _instanceDelayedDisposalException; - internal NodeServicesImpl(NodeServicesOptions options, Func nodeInstanceFactory) + internal NodeServicesImpl(Func nodeInstanceFactory) { - _options = options; _nodeInstanceFactory = nodeInstanceFactory; } diff --git a/src/Microsoft.AspNetCore.NodeServices/README.md b/src/Microsoft.AspNetCore.NodeServices/README.md index 3fb9928..6cf57cc 100644 --- a/src/Microsoft.AspNetCore.NodeServices/README.md +++ b/src/Microsoft.AspNetCore.NodeServices/README.md @@ -90,18 +90,33 @@ If you want to put `addNumber.js` inside a subfolder rather than the root of you ## For non-ASP.NET apps -In other types of .NET app where you don't have ASP.NET Core's DI system, you can get an instance of `NodeServices` as follows: +In other types of .NET Core app, where you don't have ASP.NET supplying an `IServiceCollection` to you, you'll need to instantiate your own DI container. For example, add a reference to the .NET package `Microsoft.Extensions.DependencyInjection`, and then you can construct an `IServiceCollection`, then register NodeServices as usual: ```csharp // Remember to add 'using Microsoft.AspNetCore.NodeServices;' at the top of your file +var services = new ServiceCollection(); +services.AddNodeServices(new NodeServicesOptions { /* your options here */ }); +``` -var nodeServices = Configuration.CreateNodeServices(new NodeServicesOptions()); +Now you can ask it to supply the shared `INodeServices` instance: + +```csharp +var serviceProvider = services.BuildServiceProvider(); +var nodeServices = serviceProvider.GetRequiredService(); +``` + +Or, if you want to obtain a separate (non-shared) `INodeServices` instance: + +```csharp +var options = new NodeServicesOptions { /* your options here */ }; +var nodeServices = Microsoft.AspNetCore.NodeServices.Configuration.CreateNodeServices(serviceProvider, options); ``` Besides this, the usage is the same as described for ASP.NET above, so you can now call `nodeServices.InvokeAsync(...)` etc. -You can dispose the `nodeServices` object whenever you are done with it (and it will shut down the associated Node.js instance), but because these instances are expensive to create, you should whenever possible retain and reuse instances. They are thread-safe - you can call `InvokeAsync` simultaneously from multiple threads. Also, `NodeServices` instances are smart enough to detect if the associated Node instance has died and will automatically start a new Node instance if needed. +You can dispose the `nodeServices` object whenever you are done with it (and it will shut down the associated Node.js instance), but because these instances are expensive to create, you should whenever possible retain and reuse instances. Don't dispose the shared instance returned from `serviceProvider.GetRequiredService` (except perhaps if you know your application is shutting down, although .NET's finalizers will dispose it anyway if the shutdown is graceful). +NodeServices instances are thread-safe - you can call `InvokeAsync` simultaneously from multiple threads. Also, they are smart enough to detect if the associated Node instance has died and will automatically start a new Node instance if needed. # API Reference @@ -158,21 +173,22 @@ If no `options` is passed, the default `WatchFileExtensions` array includes `.js **Signature:** ```csharp -CreateNodeServices(NodeServicesOptions options) +CreateNodeServices(IServiceProvider serviceProvider, NodeServicesOptions options) ``` -Directly supplies an instance of `NodeServices` without using ASP.NET's DI system. +Supplies a new (non-shared) instance of `NodeServices`. It takes configuration from the .NET DI system (hence requiring an `IServiceProvider`), though some aspects of configuration can be overridden via the `options` parameter. **Example** ```csharp -var nodeServices = Configuration.CreateNodeServices(new NodeServicesOptions { +var nodeServices = Configuration.CreateNodeServices(serviceProvider, new NodeServicesOptions { HostingModel = NodeHostingModel.Socket }); ``` **Parameters** - + * `serviceProvider` - type: `IServiceProvider` + * An instance of .NET's standard DI service provider. You can get an instance of this by calling `BuildServiceProvider` on an `IServiceCollection` object. See the example usage of `CreateNodeServices` earlier in this document. * `options` - type: `NodeServicesOptions`. * Configures the returned `NodeServices` instance. * Properties: diff --git a/src/Microsoft.AspNetCore.SpaServices/Prerendering/PrerenderTagHelper.cs b/src/Microsoft.AspNetCore.SpaServices/Prerendering/PrerenderTagHelper.cs index 1baaa60..8ee2484 100644 --- a/src/Microsoft.AspNetCore.SpaServices/Prerendering/PrerenderTagHelper.cs +++ b/src/Microsoft.AspNetCore.SpaServices/Prerendering/PrerenderTagHelper.cs @@ -33,10 +33,9 @@ namespace Microsoft.AspNetCore.SpaServices.Prerendering // 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 - { - ProjectPath = _applicationBasePath - }.AddDefaultEnvironmentVariables(hostEnv.IsDevelopment())); + _nodeServices = _fallbackNodeServices = Configuration.CreateNodeServices( + serviceProvider, + new NodeServicesOptions()); } } diff --git a/src/Microsoft.AspNetCore.SpaServices/Webpack/WebpackDevMiddleware.cs b/src/Microsoft.AspNetCore.SpaServices/Webpack/WebpackDevMiddleware.cs index 1a0c9bb..dc7de6e 100644 --- a/src/Microsoft.AspNetCore.SpaServices/Webpack/WebpackDevMiddleware.cs +++ b/src/Microsoft.AspNetCore.SpaServices/Webpack/WebpackDevMiddleware.cs @@ -35,19 +35,17 @@ namespace Microsoft.AspNetCore.Builder "To enable ReactHotModuleReplacement, you must also enable HotModuleReplacement."); } - var hostEnv = (IHostingEnvironment)appBuilder.ApplicationServices.GetService(typeof(IHostingEnvironment)); - var projectPath = options.ProjectPath ?? hostEnv.ContentRootPath; - // Unlike other consumers of NodeServices, WebpackDevMiddleware dosen't share Node instances, nor does it // use your DI configuration. It's important for WebpackDevMiddleware to have its own private Node instance // 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 nodeServices = Configuration.CreateNodeServices(new NodeServicesOptions - { - ProjectPath = projectPath, - WatchFileExtensions = new string[] { } // Don't watch anything - }.AddDefaultEnvironmentVariables(hostEnv.IsDevelopment())); + var nodeServices = Configuration.CreateNodeServices( + appBuilder.ApplicationServices, + new NodeServicesOptions + { + WatchFileExtensions = new string[] { } // Don't watch anything + }); // Get a filename matching the middleware Node script var script = EmbeddedResourceReader.Read(typeof(WebpackDevMiddleware), @@ -55,6 +53,8 @@ namespace Microsoft.AspNetCore.Builder var nodeScript = new StringAsTempFile(script); // Will be cleaned up on process exit // Tell Node to start the server hosting webpack-dev-middleware + var hostEnv = (IHostingEnvironment)appBuilder.ApplicationServices.GetService(typeof(IHostingEnvironment)); + var projectPath = options.ProjectPath ?? hostEnv.ContentRootPath; var devServerOptions = new { webpackConfigPath = Path.Combine(projectPath, options.ConfigFile ?? DefaultConfigFile), From f0d954b2a609c3110c1520b58e610b97879b85d6 Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Thu, 1 Sep 2016 17:46:59 +0100 Subject: [PATCH 23/55] Design review: Change AddNodeServices to take an Action like other aspects of MVC DI config --- samples/misc/LatencyTest/Program.cs | 8 +- .../Configuration/Configuration.cs | 91 ------------------- .../Configuration/NodeServicesFactory.cs | 43 +++++++++ .../Configuration/NodeServicesOptions.cs | 32 ++++++- ...NodeServicesServiceCollectionExtensions.cs | 41 +++++++++ .../HostingModels/HttpNodeInstance.cs | 2 +- .../HostingModels/OutOfProcessNodeInstance.cs | 6 +- .../HostingModels/SocketNodeInstance.cs | 2 +- .../README.md | 52 +++++------ .../Prerendering/PrerenderTagHelper.cs | 5 +- .../Webpack/WebpackDevMiddleware.cs | 9 +- 11 files changed, 150 insertions(+), 141 deletions(-) delete mode 100644 src/Microsoft.AspNetCore.NodeServices/Configuration/Configuration.cs create mode 100644 src/Microsoft.AspNetCore.NodeServices/Configuration/NodeServicesFactory.cs create mode 100644 src/Microsoft.AspNetCore.NodeServices/Configuration/NodeServicesServiceCollectionExtensions.cs diff --git a/samples/misc/LatencyTest/Program.cs b/samples/misc/LatencyTest/Program.cs index 9dc3ce8..e435d37 100755 --- a/samples/misc/LatencyTest/Program.cs +++ b/samples/misc/LatencyTest/Program.cs @@ -15,10 +15,10 @@ namespace ConsoleApplication public static void Main(string[] args) { // Set up the DI system var services = new ServiceCollection(); - services.AddNodeServices(new NodeServicesOptions { - HostingModel = NodeServicesOptions.DefaultNodeHostingModel, - ProjectPath = Directory.GetCurrentDirectory(), - WatchFileExtensions = new string[] {} // Don't watch anything + services.AddNodeServices(options => { + options.HostingModel = NodeServicesOptions.DefaultNodeHostingModel; + options.ProjectPath = Directory.GetCurrentDirectory(); + options.WatchFileExtensions = new string[] {}; // Don't watch anything }); var serviceProvider = services.BuildServiceProvider(); diff --git a/src/Microsoft.AspNetCore.NodeServices/Configuration/Configuration.cs b/src/Microsoft.AspNetCore.NodeServices/Configuration/Configuration.cs deleted file mode 100644 index 4cbba69..0000000 --- a/src/Microsoft.AspNetCore.NodeServices/Configuration/Configuration.cs +++ /dev/null @@ -1,91 +0,0 @@ -using System; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.NodeServices.HostingModels; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Console; -using System.Collections.Generic; - -namespace Microsoft.AspNetCore.NodeServices -{ - public static class Configuration - { - const string LogCategoryName = "Microsoft.AspNetCore.NodeServices"; - - public static void AddNodeServices(this IServiceCollection serviceCollection) - => AddNodeServices(serviceCollection, new NodeServicesOptions()); - - public static void AddNodeServices(this IServiceCollection serviceCollection, NodeServicesOptions options) - { - serviceCollection.AddSingleton( - typeof(INodeServices), - serviceProvider => CreateNodeServices(serviceProvider, options)); - } - - public static INodeServices CreateNodeServices(IServiceProvider serviceProvider, NodeServicesOptions options) - { - return new NodeServicesImpl(() => CreateNodeInstance(serviceProvider, options)); - } - - private static INodeInstance CreateNodeInstance(IServiceProvider serviceProvider, NodeServicesOptions options) - { - if (options.NodeInstanceFactory != null) - { - // If you've explicitly supplied an INodeInstance factory, we'll use that. This is useful for - // custom INodeInstance implementations. - return options.NodeInstanceFactory(); - } - else - { - // Otherwise we'll construct the type of INodeInstance specified by the HostingModel property - // (which itself has a useful default value), plus obtain config information from the DI system. - var projectPath = options.ProjectPath; - var envVars = options.EnvironmentVariables == null - ? new Dictionary() - : new Dictionary(options.EnvironmentVariables); - - var hostEnv = serviceProvider.GetService(); - if (hostEnv != null) - { - // In an ASP.NET environment, we can use the IHostingEnvironment data to auto-populate a few - // things that you'd otherwise have to specify manually - if (string.IsNullOrEmpty(projectPath)) - { - projectPath = hostEnv.ContentRootPath; - } - - // Similarly, we can determine the 'is development' value from the hosting environment - if (!envVars.ContainsKey("NODE_ENV")) - { - // These strings are a de-facto standard in Node - envVars["NODE_ENV"] = hostEnv.IsDevelopment() ? "development" : "production"; - } - } - - // If no logger was specified explicitly, we should use the one from DI. - // If it doesn't provide one, we'll set up a default one. - var logger = options.NodeInstanceOutputLogger; - if (logger == null) - { - var loggerFactory = serviceProvider.GetService(); - logger = loggerFactory != null - ? loggerFactory.CreateLogger(LogCategoryName) - : new ConsoleLogger(LogCategoryName, null, false); - } - - switch (options.HostingModel) - { - case NodeHostingModel.Http: - return new HttpNodeInstance(projectPath, options.WatchFileExtensions, logger, - envVars, options.LaunchWithDebugging, options.DebuggingPort, /* port */ 0); - case NodeHostingModel.Socket: - var pipeName = "pni-" + Guid.NewGuid().ToString("D"); // Arbitrary non-clashing string - return new SocketNodeInstance(projectPath, options.WatchFileExtensions, pipeName, logger, - envVars, options.LaunchWithDebugging, options.DebuggingPort); - default: - throw new ArgumentException("Unknown hosting model: " + options.HostingModel); - } - } - } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.NodeServices/Configuration/NodeServicesFactory.cs b/src/Microsoft.AspNetCore.NodeServices/Configuration/NodeServicesFactory.cs new file mode 100644 index 0000000..fc038f2 --- /dev/null +++ b/src/Microsoft.AspNetCore.NodeServices/Configuration/NodeServicesFactory.cs @@ -0,0 +1,43 @@ +using System; +using Microsoft.AspNetCore.NodeServices.HostingModels; + +namespace Microsoft.AspNetCore.NodeServices +{ + public static class NodeServicesFactory + { + public static INodeServices CreateNodeServices(NodeServicesOptions options) + { + if (options == null) + { + throw new ArgumentNullException(nameof (options)); + } + + return new NodeServicesImpl(() => CreateNodeInstance(options)); + } + + private static INodeInstance CreateNodeInstance(NodeServicesOptions options) + { + if (options.NodeInstanceFactory != null) + { + // If you've explicitly supplied an INodeInstance factory, we'll use that. This is useful for + // custom INodeInstance implementations. + return options.NodeInstanceFactory(); + } + else + { + switch (options.HostingModel) + { + case NodeHostingModel.Http: + return new HttpNodeInstance(options.ProjectPath, options.WatchFileExtensions, options.NodeInstanceOutputLogger, + options.EnvironmentVariables, options.LaunchWithDebugging, options.DebuggingPort, /* port */ 0); + case NodeHostingModel.Socket: + var pipeName = "pni-" + Guid.NewGuid().ToString("D"); // Arbitrary non-clashing string + return new SocketNodeInstance(options.ProjectPath, options.WatchFileExtensions, pipeName, options.NodeInstanceOutputLogger, + options.EnvironmentVariables, options.LaunchWithDebugging, options.DebuggingPort); + default: + throw new ArgumentException("Unknown hosting model: " + options.HostingModel); + } + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.NodeServices/Configuration/NodeServicesOptions.cs b/src/Microsoft.AspNetCore.NodeServices/Configuration/NodeServicesOptions.cs index ddc9686..edf9ae2 100644 --- a/src/Microsoft.AspNetCore.NodeServices/Configuration/NodeServicesOptions.cs +++ b/src/Microsoft.AspNetCore.NodeServices/Configuration/NodeServicesOptions.cs @@ -2,21 +2,45 @@ using System; using System.Collections.Generic; using Microsoft.AspNetCore.NodeServices.HostingModels; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Logging.Console; namespace Microsoft.AspNetCore.NodeServices { public class NodeServicesOptions { public const NodeHostingModel DefaultNodeHostingModel = NodeHostingModel.Http; - + private const string LogCategoryName = "Microsoft.AspNetCore.NodeServices"; private static readonly string[] DefaultWatchFileExtensions = { ".js", ".jsx", ".ts", ".tsx", ".json", ".html" }; - public NodeServicesOptions() + public NodeServicesOptions(IServiceProvider serviceProvider) { + if (serviceProvider == null) + { + throw new ArgumentNullException(nameof (serviceProvider)); + } + + EnvironmentVariables = new Dictionary(); HostingModel = DefaultNodeHostingModel; WatchFileExtensions = (string[])DefaultWatchFileExtensions.Clone(); + + // In an ASP.NET environment, we can use the IHostingEnvironment data to auto-populate a few + // things that you'd otherwise have to specify manually + var hostEnv = serviceProvider.GetService(); + if (hostEnv != null) + { + ProjectPath = hostEnv.ContentRootPath; + EnvironmentVariables["NODE_ENV"] = hostEnv.IsDevelopment() ? "development" : "production"; // De-facto standard values for Node + } + + // If the DI system gives us a logger, use it. Otherwise, set up a default one. + var loggerFactory = serviceProvider.GetService(); + NodeInstanceOutputLogger = loggerFactory != null + ? loggerFactory.CreateLogger(LogCategoryName) + : new ConsoleLogger(LogCategoryName, null, false); } - public Action OnBeforeStartExternalProcess { get; set; } + public NodeHostingModel HostingModel { get; set; } public Func NodeInstanceFactory { get; set; } public string ProjectPath { get; set; } @@ -24,6 +48,6 @@ namespace Microsoft.AspNetCore.NodeServices public ILogger NodeInstanceOutputLogger { get; set; } public bool LaunchWithDebugging { get; set; } public IDictionary EnvironmentVariables { get; set; } - public int? DebuggingPort { get; set; } + public int DebuggingPort { get; set; } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.NodeServices/Configuration/NodeServicesServiceCollectionExtensions.cs b/src/Microsoft.AspNetCore.NodeServices/Configuration/NodeServicesServiceCollectionExtensions.cs new file mode 100644 index 0000000..e2f516e --- /dev/null +++ b/src/Microsoft.AspNetCore.NodeServices/Configuration/NodeServicesServiceCollectionExtensions.cs @@ -0,0 +1,41 @@ +using System; +using Microsoft.AspNetCore.NodeServices; + +namespace Microsoft.Extensions.DependencyInjection +{ + /// + /// Extension methods for setting up NodeServices in an . + /// + public static class NodeServicesServiceCollectionExtensions + { + public static void AddNodeServices(this IServiceCollection serviceCollection) + => AddNodeServices(serviceCollection, _ => {}); + + [Obsolete("Use the AddNodeServices(Action setupAction) overload instead.")] + public static void AddNodeServices(this IServiceCollection serviceCollection, NodeServicesOptions options) + { + serviceCollection.AddSingleton(typeof (INodeServices), _ => + { + return NodeServicesFactory.CreateNodeServices(options); + }); + } + + public static void AddNodeServices(this IServiceCollection serviceCollection, Action setupAction) + { + if (setupAction == null) + { + throw new ArgumentNullException(nameof (setupAction)); + } + + serviceCollection.AddSingleton(typeof(INodeServices), serviceProvider => + { + // First we let NodeServicesOptions take its defaults from the IServiceProvider, + // then we let the developer override those options + var options = new NodeServicesOptions(serviceProvider); + setupAction(options); + + return NodeServicesFactory.CreateNodeServices(options); + }); + } + } +} diff --git a/src/Microsoft.AspNetCore.NodeServices/HostingModels/HttpNodeInstance.cs b/src/Microsoft.AspNetCore.NodeServices/HostingModels/HttpNodeInstance.cs index 9400d6e..9fd72a4 100644 --- a/src/Microsoft.AspNetCore.NodeServices/HostingModels/HttpNodeInstance.cs +++ b/src/Microsoft.AspNetCore.NodeServices/HostingModels/HttpNodeInstance.cs @@ -35,7 +35,7 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels private int _portNumber; public HttpNodeInstance(string projectPath, string[] watchFileExtensions, ILogger nodeInstanceOutputLogger, - IDictionary environmentVars, bool launchWithDebugging, int? debuggingPort, int port = 0) + IDictionary environmentVars, bool launchWithDebugging, int debuggingPort, int port = 0) : base( EmbeddedResourceReader.Read( typeof(HttpNodeInstance), diff --git a/src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs b/src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs index 73dcedc..641c098 100644 --- a/src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs +++ b/src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs @@ -49,7 +49,7 @@ If you haven't yet installed node-inspector, you can do so as follows: ILogger nodeOutputLogger, IDictionary environmentVars, bool launchWithDebugging, - int? debuggingPort) + int debuggingPort) { if (nodeOutputLogger == null) { @@ -101,12 +101,12 @@ If you haven't yet installed node-inspector, you can do so as follows: // This method is virtual, as it provides a way to override the NODE_PATH or the path to node.exe protected virtual ProcessStartInfo PrepareNodeProcessStartInfo( string entryPointFilename, string projectPath, string commandLineArguments, - IDictionary environmentVars, bool launchWithDebugging, int? debuggingPort) + IDictionary environmentVars, bool launchWithDebugging, int debuggingPort) { string debuggingArgs; if (launchWithDebugging) { - debuggingArgs = debuggingPort.HasValue ? $"--debug={debuggingPort.Value} " : "--debug "; + debuggingArgs = debuggingPort != default(int) ? $"--debug={debuggingPort} " : "--debug "; _nodeDebuggingPort = debuggingPort; } else diff --git a/src/Microsoft.AspNetCore.NodeServices/HostingModels/SocketNodeInstance.cs b/src/Microsoft.AspNetCore.NodeServices/HostingModels/SocketNodeInstance.cs index 46115f6..1a1c74e 100644 --- a/src/Microsoft.AspNetCore.NodeServices/HostingModels/SocketNodeInstance.cs +++ b/src/Microsoft.AspNetCore.NodeServices/HostingModels/SocketNodeInstance.cs @@ -40,7 +40,7 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels public SocketNodeInstance(string projectPath, string[] watchFileExtensions, string socketAddress, ILogger nodeInstanceOutputLogger, IDictionary environmentVars, - bool launchWithDebugging, int? debuggingPort) + bool launchWithDebugging, int debuggingPort) : base( EmbeddedResourceReader.Read( typeof(SocketNodeInstance), diff --git a/src/Microsoft.AspNetCore.NodeServices/README.md b/src/Microsoft.AspNetCore.NodeServices/README.md index 6cf57cc..1a20a8f 100644 --- a/src/Microsoft.AspNetCore.NodeServices/README.md +++ b/src/Microsoft.AspNetCore.NodeServices/README.md @@ -95,7 +95,9 @@ In other types of .NET Core app, where you don't have ASP.NET supplying an `ISer ```csharp // Remember to add 'using Microsoft.AspNetCore.NodeServices;' at the top of your file var services = new ServiceCollection(); -services.AddNodeServices(new NodeServicesOptions { /* your options here */ }); +services.AddNodeServices(options => { + // Set any properties that you want on 'options' here +}); ``` Now you can ask it to supply the shared `INodeServices` instance: @@ -108,8 +110,8 @@ var nodeServices = serviceProvider.GetRequiredService(); Or, if you want to obtain a separate (non-shared) `INodeServices` instance: ```csharp -var options = new NodeServicesOptions { /* your options here */ }; -var nodeServices = Microsoft.AspNetCore.NodeServices.Configuration.CreateNodeServices(serviceProvider, options); +var options = new NodeServicesOptions(serviceProvider) { /* Assign/override any other options here */ }; +var nodeServices = NodeServicesFactory.CreateNodeServices(options); ``` Besides this, the usage is the same as described for ASP.NET above, so you can now call `nodeServices.InvokeAsync(...)` etc. @@ -126,7 +128,7 @@ NodeServices instances are thread-safe - you can call `InvokeAsync` simultane ```csharp AddNodeServices() -AddNodeServices(NodeServicesOptions options) +AddNodeServices(Action setupAction) ``` This is an extension method on `IServiceCollection`. It registers NodeServices with ASP.NET Core's DI system. Typically you should call this from the `ConfigureServices` method in your `Startup.cs` file. @@ -148,23 +150,21 @@ services.AddNodeServices(); Or, specifying options: ```csharp -services.AddNodeServices(new NodeServicesOptions +services.AddNodeServices(options => { - WatchFileExtensions = new[] { ".coffee", ".sass" }, + options.WatchFileExtensions = new[] { ".coffee", ".sass" }; // ... etc. - see other properties below }); ``` **Parameters** - * `options` - type: `NodeServicesOptions` - * Optional. If specified, configures how the `NodeServices` instances will work. - * Properties: + * `setupAction` - type: `Action` + * Optional. If not specified, defaults will be used. + * Properties on `NodeServicesOptions`: * `HostingModel` - an `NodeHostingModel` enum value. See: [hosting models](#hosting-models) * `ProjectPath` - if specified, controls the working directory used when launching Node instances. This affects, for example, the location that `require` statements resolve relative paths against. If not specified, your application root directory is used. - * `WatchFileExtensions` - if specified, the launched Node instance will watch for changes to any files with these extensions, and auto-restarts when any are changed. - -If no `options` is passed, the default `WatchFileExtensions` array includes `.js`, `.jsx`, `.ts`, `.tsx`, `.json`, and `.html`. + * `WatchFileExtensions` - if specified, the launched Node instance will watch for changes to any files with these extensions, and auto-restarts when any are changed. The default array includes `.js`, `.jsx`, `.ts`, `.tsx`, `.json`, and `.html`. **Return type**: None. But once you've done this, you can get `NodeServices` instances out of ASP.NET's DI system. Typically it will be a singleton instance. @@ -173,22 +173,19 @@ If no `options` is passed, the default `WatchFileExtensions` array includes `.js **Signature:** ```csharp -CreateNodeServices(IServiceProvider serviceProvider, NodeServicesOptions options) +CreateNodeServices(NodeServicesOptions options) ``` -Supplies a new (non-shared) instance of `NodeServices`. It takes configuration from the .NET DI system (hence requiring an `IServiceProvider`), though some aspects of configuration can be overridden via the `options` parameter. +Supplies a new (non-shared) instance of `NodeServices`. **Example** ```csharp -var nodeServices = Configuration.CreateNodeServices(serviceProvider, new NodeServicesOptions { - HostingModel = NodeHostingModel.Socket -}); +var options = new NodeServicesOptions(serviceProvider); // Obtains default options from DI config +var nodeServices = NodeServicesFactory.CreateNodeServices(options); ``` **Parameters** - * `serviceProvider` - type: `IServiceProvider` - * An instance of .NET's standard DI service provider. You can get an instance of this by calling `BuildServiceProvider` on an `IServiceCollection` object. See the example usage of `CreateNodeServices` earlier in this document. * `options` - type: `NodeServicesOptions`. * Configures the returned `NodeServices` instance. * Properties: @@ -343,12 +340,12 @@ People have asked about using [VroomJS](https://github.com/fogzot/vroomjs) as a ### Built-in hosting models -Normally, you can just use the default hosting model, and not worry about it. But if you have some special requirements, select a hosting model by passing an `options` parameter to `AddNodeServices` or `CreateNodeServices`, and populate its `HostingModel` property. Example: +Normally, you can just use the default hosting model, and not worry about it. But if you have some special requirements, select a hosting model by setting the `HostingModel` property on the `options` object in `AddNodeServices`. Example: ```csharp -services.AddNodeServices(new NodeServicesOptions +services.AddNodeServices(options => { - HostingModel = NodeHostingModel.Socket + options.HostingModel = NodeHostingModel.Socket; }); ``` @@ -365,12 +362,11 @@ The default transport may change from `Http` to `Socket` in the near future, bec ### Custom hosting models -If you implement a custom hosting model (by implementing `INodeInstance`), then you can cause it to be used by populating `NodeInstanceFactory` on a `NodeServicesOptions`: +If you implement a custom hosting model (by implementing `INodeInstance`), then you can cause it to be used by populating `NodeInstanceFactory` on your options: ```csharp -var options = new NodeServicesOptions { - NodeInstanceFactory = () => new MyCustomNodeInstance() -}; +services.AddNodeServices(options => +{ + options.NodeInstanceFactory = () => new MyCustomNodeInstance(); +}); ``` - -Now you can pass this `options` object to [`AddNodeServices`](#addnodeservices) or [`CreateNodeServices`](#createnodeservices). diff --git a/src/Microsoft.AspNetCore.SpaServices/Prerendering/PrerenderTagHelper.cs b/src/Microsoft.AspNetCore.SpaServices/Prerendering/PrerenderTagHelper.cs index 8ee2484..b91c2a6 100644 --- a/src/Microsoft.AspNetCore.SpaServices/Prerendering/PrerenderTagHelper.cs +++ b/src/Microsoft.AspNetCore.SpaServices/Prerendering/PrerenderTagHelper.cs @@ -33,9 +33,8 @@ namespace Microsoft.AspNetCore.SpaServices.Prerendering // in your startup file, but then again it might be confusing that you don't need to. if (_nodeServices == null) { - _nodeServices = _fallbackNodeServices = Configuration.CreateNodeServices( - serviceProvider, - new NodeServicesOptions()); + _nodeServices = _fallbackNodeServices = NodeServicesFactory.CreateNodeServices( + new NodeServicesOptions(serviceProvider)); } } diff --git a/src/Microsoft.AspNetCore.SpaServices/Webpack/WebpackDevMiddleware.cs b/src/Microsoft.AspNetCore.SpaServices/Webpack/WebpackDevMiddleware.cs index dc7de6e..dcc5d4f 100644 --- a/src/Microsoft.AspNetCore.SpaServices/Webpack/WebpackDevMiddleware.cs +++ b/src/Microsoft.AspNetCore.SpaServices/Webpack/WebpackDevMiddleware.cs @@ -40,12 +40,9 @@ 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 nodeServices = Configuration.CreateNodeServices( - appBuilder.ApplicationServices, - new NodeServicesOptions - { - WatchFileExtensions = new string[] { } // Don't watch anything - }); + var nodeServicesOptions = new NodeServicesOptions(appBuilder.ApplicationServices); + nodeServicesOptions.WatchFileExtensions = new string[] {}; // Don't watch anything + var nodeServices = NodeServicesFactory.CreateNodeServices(nodeServicesOptions); // Get a filename matching the middleware Node script var script = EmbeddedResourceReader.Read(typeof(WebpackDevMiddleware), From 03dcae24071968349b0eeeb8e03018cf562075f4 Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Thu, 1 Sep 2016 17:52:26 +0100 Subject: [PATCH 24/55] Simplify docs --- src/Microsoft.AspNetCore.NodeServices/README.md | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/src/Microsoft.AspNetCore.NodeServices/README.md b/src/Microsoft.AspNetCore.NodeServices/README.md index 1a20a8f..c2c6bbc 100644 --- a/src/Microsoft.AspNetCore.NodeServices/README.md +++ b/src/Microsoft.AspNetCore.NodeServices/README.md @@ -43,15 +43,9 @@ In that case, you don't need to use NodeServices directly (or install it manuall ## For ASP.NET Core apps -ASP.NET Core has a built-in dependency injection (DI) system. NodeServices is designed to work with this, so you don't have to manage the creation or disposal of instances. +.NET Core has a built-in dependency injection (DI) system. NodeServices is designed to work with this, so you don't have to manage the creation or disposal of instances. -Enable NodeServices in your application by first adding the following to the top of your `Startup.cs` file: - -```csharp -using Microsoft.AspNetCore.NodeServices; -``` - -... and then add to your `ConfigureServices` method in that file: +Enable NodeServices in your application by first adding the following to your `ConfigureServices` method in `Startup.cs`: ```csharp public void ConfigureServices(IServiceCollection services) @@ -93,7 +87,6 @@ If you want to put `addNumber.js` inside a subfolder rather than the root of you In other types of .NET Core app, where you don't have ASP.NET supplying an `IServiceCollection` to you, you'll need to instantiate your own DI container. For example, add a reference to the .NET package `Microsoft.Extensions.DependencyInjection`, and then you can construct an `IServiceCollection`, then register NodeServices as usual: ```csharp -// Remember to add 'using Microsoft.AspNetCore.NodeServices;' at the top of your file var services = new ServiceCollection(); services.AddNodeServices(options => { // Set any properties that you want on 'options' here @@ -133,10 +126,10 @@ AddNodeServices(Action setupAction) This is an extension method on `IServiceCollection`. It registers NodeServices with ASP.NET Core's DI system. Typically you should call this from the `ConfigureServices` method in your `Startup.cs` file. -To access this extension method, you'll need to add the following namespace import to the top of your file: +To access this extension method, you'll need to add the following namespace import to the top of your file, if it isn't already there: ```csharp -using Microsoft.AspNetCore.NodeServices; +using Microsoft.Extensions.DependencyInjection; ``` **Examples** From 377401b5e6b8668f6782a210bc81cc7a766b167e Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Wed, 7 Sep 2016 14:00:09 +0100 Subject: [PATCH 25/55] Update README to clarify that you need .NET Core 1.0 RTM --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9322418..287a5fe 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ This repo contains: * A Yeoman generator that creates preconfigured app starting points ([guide](http://blog.stevensanderson.com/2016/05/02/angular2-react-knockout-apps-on-aspnet-core/)) * Samples and docs -Everything here is cross-platform, and works with .NET Core 1.0 RC2 or later on Windows, Linux, or OS X. +Everything here is cross-platform, and works with .NET Core 1.0 (RTM) or later on Windows, Linux, or OS X. ## Creating new applications From 465d0c8d15820c95fd8fc927eacf59df9c338f1a Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Wed, 7 Sep 2016 17:11:57 +0100 Subject: [PATCH 26/55] Design review: Explicitly disable TypeNameHandling in all Json.NET usage --- .../HostingModels/HttpNodeInstance.cs | 9 +++++---- .../HostingModels/SocketNodeInstance.cs | 3 ++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Microsoft.AspNetCore.NodeServices/HostingModels/HttpNodeInstance.cs b/src/Microsoft.AspNetCore.NodeServices/HostingModels/HttpNodeInstance.cs index 9fd72a4..0ab2782 100644 --- a/src/Microsoft.AspNetCore.NodeServices/HostingModels/HttpNodeInstance.cs +++ b/src/Microsoft.AspNetCore.NodeServices/HostingModels/HttpNodeInstance.cs @@ -25,9 +25,10 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels private static readonly Regex PortMessageRegex = new Regex(@"^\[Microsoft.AspNetCore.NodeServices.HttpNodeHost:Listening on port (\d+)\]$"); - private static readonly JsonSerializerSettings JsonSerializerSettings = new JsonSerializerSettings + private static readonly JsonSerializerSettings jsonSerializerSettings = new JsonSerializerSettings { - ContractResolver = new CamelCasePropertyNamesContractResolver() + ContractResolver = new CamelCasePropertyNamesContractResolver(), + TypeNameHandling = TypeNameHandling.None }; private readonly HttpClient _client; @@ -58,7 +59,7 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels protected override async Task InvokeExportAsync(NodeInvocationInfo invocationInfo) { - 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:" + _portNumber, payload); @@ -85,7 +86,7 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels case "application/json": var responseJson = await response.Content.ReadAsStringAsync(); - return JsonConvert.DeserializeObject(responseJson); + return JsonConvert.DeserializeObject(responseJson, jsonSerializerSettings); case "application/octet-stream": // Streamed responses have to be received as System.IO.Stream instances diff --git a/src/Microsoft.AspNetCore.NodeServices/HostingModels/SocketNodeInstance.cs b/src/Microsoft.AspNetCore.NodeServices/HostingModels/SocketNodeInstance.cs index 1a1c74e..2acfb78 100644 --- a/src/Microsoft.AspNetCore.NodeServices/HostingModels/SocketNodeInstance.cs +++ b/src/Microsoft.AspNetCore.NodeServices/HostingModels/SocketNodeInstance.cs @@ -29,7 +29,8 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels { private readonly static JsonSerializerSettings jsonSerializerSettings = new JsonSerializerSettings { - ContractResolver = new CamelCasePropertyNamesContractResolver() + ContractResolver = new CamelCasePropertyNamesContractResolver(), + TypeNameHandling = TypeNameHandling.None }; private readonly SemaphoreSlim _connectionCreationSemaphore = new SemaphoreSlim(1); From f358d8e2b2b5913b01aade75b40a2b2e05418fd2 Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Wed, 7 Sep 2016 17:59:13 +0100 Subject: [PATCH 27/55] In HttpNodeInstance, correctly report response serialisation errors back to .NET (previously, it just timed out) --- .../Content/Node/entrypoint-http.js | 23 ++++++++++++------- .../TypeScript/HttpNodeInstanceEntryPoint.ts | 23 ++++++++++++------- 2 files changed, 30 insertions(+), 16 deletions(-) diff --git a/src/Microsoft.AspNetCore.NodeServices/Content/Node/entrypoint-http.js b/src/Microsoft.AspNetCore.NodeServices/Content/Node/entrypoint-http.js index 76a00ef..9d231f3 100644 --- a/src/Microsoft.AspNetCore.NodeServices/Content/Node/entrypoint-http.js +++ b/src/Microsoft.AspNetCore.NodeServices/Content/Node/entrypoint-http.js @@ -69,18 +69,21 @@ if (!hasSentResult) { hasSentResult = true; if (errorValue) { - res.statusCode = 500; - if (errorValue.stack) { - res.end(errorValue.stack); - } - else { - res.end(errorValue.toString()); - } + respondWithError(res, errorValue); } else if (typeof successValue !== 'string') { // Arbitrary object/number/etc - JSON-serialize it + var successValueJson = void 0; + try { + successValueJson = JSON.stringify(successValue); + } + catch (ex) { + // JSON serialization error - pass it back to .NET + respondWithError(res, ex); + return; + } res.setHeader('Content-Type', 'application/json'); - res.end(JSON.stringify(successValue)); + res.end(JSON.stringify(successValueJson)); } else { // String - can bypass JSON-serialization altogether @@ -129,6 +132,10 @@ .on('data', function (chunk) { requestBodyAsString += chunk; }) .on('end', function () { callback(JSON.parse(requestBodyAsString)); }); } + function respondWithError(res, errorValue) { + res.statusCode = 500; + res.end(errorValue.stack || errorValue.toString()); + } /***/ }, diff --git a/src/Microsoft.AspNetCore.NodeServices/TypeScript/HttpNodeInstanceEntryPoint.ts b/src/Microsoft.AspNetCore.NodeServices/TypeScript/HttpNodeInstanceEntryPoint.ts index 431810e..5bb8623 100644 --- a/src/Microsoft.AspNetCore.NodeServices/TypeScript/HttpNodeInstanceEntryPoint.ts +++ b/src/Microsoft.AspNetCore.NodeServices/TypeScript/HttpNodeInstanceEntryPoint.ts @@ -17,17 +17,19 @@ const server = http.createServer((req, res) => { if (!hasSentResult) { hasSentResult = true; if (errorValue) { - res.statusCode = 500; - - if (errorValue.stack) { - res.end(errorValue.stack); - } else { - res.end(errorValue.toString()); - } + respondWithError(res, errorValue); } else if (typeof successValue !== 'string') { // Arbitrary object/number/etc - JSON-serialize it + let successValueJson: string; + try { + successValueJson = JSON.stringify(successValue); + } catch (ex) { + // JSON serialization error - pass it back to .NET + respondWithError(res, ex); + return; + } res.setHeader('Content-Type', 'application/json'); - res.end(JSON.stringify(successValue)); + res.end(JSON.stringify(successValueJson)); } else { // String - can bypass JSON-serialization altogether res.setHeader('Content-Type', 'text/plain'); @@ -82,3 +84,8 @@ function readRequestBodyAsJson(request, callback) { .on('data', chunk => { requestBodyAsString += chunk; }) .on('end', () => { callback(JSON.parse(requestBodyAsString)); }); } + +function respondWithError(res: http.ServerResponse, errorValue: any) { + res.statusCode = 500; + res.end(errorValue.stack || errorValue.toString()); +} From 279986129659faae8a110ca62cb4cf3965e0604f Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Thu, 8 Sep 2016 10:56:50 +0100 Subject: [PATCH 28/55] Support cancellation of NodeServices invocations --- .../HostingModels/HttpNodeInstance.cs | 15 ++++++---- .../HostingModels/INodeInstance.cs | 3 +- .../HostingModels/OutOfProcessNodeInstance.cs | 16 ++++++---- .../HostingModels/SocketNodeInstance.cs | 25 +++++++++------- .../INodeServices.cs | 4 +++ .../NodeServicesImpl.cs | 21 +++++++++---- .../Util/TaskExtensions.cs | 30 +++++++++++++++++++ 7 files changed, 87 insertions(+), 27 deletions(-) create mode 100644 src/Microsoft.AspNetCore.NodeServices/Util/TaskExtensions.cs diff --git a/src/Microsoft.AspNetCore.NodeServices/HostingModels/HttpNodeInstance.cs b/src/Microsoft.AspNetCore.NodeServices/HostingModels/HttpNodeInstance.cs index 0ab2782..0c11371 100644 --- a/src/Microsoft.AspNetCore.NodeServices/HostingModels/HttpNodeInstance.cs +++ b/src/Microsoft.AspNetCore.NodeServices/HostingModels/HttpNodeInstance.cs @@ -4,6 +4,7 @@ using System.IO; using System.Net.Http; using System.Text; using System.Text.RegularExpressions; +using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Newtonsoft.Json; @@ -57,15 +58,17 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels return $"--port {port}"; } - protected override async Task InvokeExportAsync(NodeInvocationInfo invocationInfo) + protected override async Task InvokeExportAsync( + NodeInvocationInfo invocationInfo, CancellationToken cancellationToken) { var payloadJson = JsonConvert.SerializeObject(invocationInfo, jsonSerializerSettings); var payload = new StringContent(payloadJson, Encoding.UTF8, "application/json"); - var response = await _client.PostAsync("http://localhost:" + _portNumber, payload); + var response = await _client.PostAsync("http://localhost:" + _portNumber, payload, cancellationToken); if (!response.IsSuccessStatusCode) { - var responseErrorString = await response.Content.ReadAsStringAsync(); + // Unfortunately there's no true way to cancel ReadAsStringAsync calls, hence AbandonIfCancelled + var responseErrorString = await response.Content.ReadAsStringAsync().OrThrowOnCancellation(cancellationToken); throw new Exception("Call to Node module failed with error: " + responseErrorString); } @@ -81,11 +84,11 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels typeof(T).FullName); } - var responseString = await response.Content.ReadAsStringAsync(); + var responseString = await response.Content.ReadAsStringAsync().OrThrowOnCancellation(cancellationToken); return (T)(object)responseString; case "application/json": - var responseJson = await response.Content.ReadAsStringAsync(); + var responseJson = await response.Content.ReadAsStringAsync().OrThrowOnCancellation(cancellationToken); return JsonConvert.DeserializeObject(responseJson, jsonSerializerSettings); case "application/octet-stream": @@ -97,7 +100,7 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels typeof(T).FullName + ". Instead you must use the generic type System.IO.Stream."); } - return (T)(object)(await response.Content.ReadAsStreamAsync()); + return (T)(object)(await response.Content.ReadAsStreamAsync().OrThrowOnCancellation(cancellationToken)); default: throw new InvalidOperationException("Unexpected response content type: " + responseContentType.MediaType); diff --git a/src/Microsoft.AspNetCore.NodeServices/HostingModels/INodeInstance.cs b/src/Microsoft.AspNetCore.NodeServices/HostingModels/INodeInstance.cs index cac69f2..68a4319 100644 --- a/src/Microsoft.AspNetCore.NodeServices/HostingModels/INodeInstance.cs +++ b/src/Microsoft.AspNetCore.NodeServices/HostingModels/INodeInstance.cs @@ -1,10 +1,11 @@ using System; +using System.Threading; using System.Threading.Tasks; namespace Microsoft.AspNetCore.NodeServices.HostingModels { public interface INodeInstance : IDisposable { - Task InvokeExportAsync(string moduleName, string exportNameOrNull, params object[] args); + Task InvokeExportAsync(CancellationToken cancellationToken, string moduleName, string exportNameOrNull, params object[] args); } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs b/src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs index 641c098..9a583d3 100644 --- a/src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs +++ b/src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; @@ -67,7 +68,8 @@ If you haven't yet installed node-inspector, you can do so as follows: ConnectToInputOutputStreams(); } - public async Task InvokeExportAsync(string moduleName, string exportNameOrNull, params object[] args) + public async Task InvokeExportAsync( + CancellationToken cancellationToken, string moduleName, string exportNameOrNull, params object[] args) { if (_nodeProcess.HasExited || _nodeProcessNeedsRestart) { @@ -79,15 +81,17 @@ If you haven't yet installed node-inspector, you can do so as follows: throw new NodeInvocationException(message, null, nodeInstanceUnavailable: true); } - // Wait until the connection is established. This will throw if the connection fails to initialize. - await _connectionIsReadySource.Task; + // Wait until the connection is established. This will throw if the connection fails to initialize, + // or if cancellation is requested first. Note that we can't really cancel the "establishing connection" + // task because that's shared with all callers, but we can stop waiting for it if this call is cancelled. + await _connectionIsReadySource.Task.OrThrowOnCancellation(cancellationToken); return await InvokeExportAsync(new NodeInvocationInfo { ModuleName = moduleName, ExportedFunctionName = exportNameOrNull, Args = args - }); + }, cancellationToken); } public void Dispose() @@ -96,7 +100,9 @@ If you haven't yet installed node-inspector, you can do so as follows: GC.SuppressFinalize(this); } - protected abstract Task InvokeExportAsync(NodeInvocationInfo invocationInfo); + protected abstract Task InvokeExportAsync( + NodeInvocationInfo invocationInfo, + CancellationToken cancellationToken); // This method is virtual, as it provides a way to override the NODE_PATH or the path to node.exe protected virtual ProcessStartInfo PrepareNodeProcessStartInfo( diff --git a/src/Microsoft.AspNetCore.NodeServices/HostingModels/SocketNodeInstance.cs b/src/Microsoft.AspNetCore.NodeServices/HostingModels/SocketNodeInstance.cs index 2acfb78..2acfc0e 100644 --- a/src/Microsoft.AspNetCore.NodeServices/HostingModels/SocketNodeInstance.cs +++ b/src/Microsoft.AspNetCore.NodeServices/HostingModels/SocketNodeInstance.cs @@ -57,7 +57,7 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels _socketAddress = socketAddress; } - protected override async Task InvokeExportAsync(NodeInvocationInfo invocationInfo) + protected override async Task InvokeExportAsync(NodeInvocationInfo invocationInfo, CancellationToken cancellationToken) { if (_connectionHasFailed) { @@ -70,7 +70,12 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels if (_virtualConnectionClient == null) { - await EnsureVirtualConnectionClientCreated(); + // Although we could pass the cancellationToken into EnsureVirtualConnectionClientCreated and + // have it signal cancellations upstream, that would be a bad thing to do, because all callers + // wait for the same connection task. There's no reason why the first caller should have the + // special ability to cancel the connection process in a way that would affect subsequent + // callers. So, each caller just independently stops awaiting connection if that call is cancelled. + await EnsureVirtualConnectionClientCreated().OrThrowOnCancellation(cancellationToken); } // For each invocation, we open a new virtual connection. This gives an API equivalent to opening a new @@ -83,7 +88,7 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels virtualConnection = _virtualConnectionClient.OpenVirtualConnection(); // Send request - await WriteJsonLineAsync(virtualConnection, invocationInfo); + await WriteJsonLineAsync(virtualConnection, invocationInfo, cancellationToken); // Determine what kind of response format is expected if (typeof(T) == typeof(Stream)) @@ -96,7 +101,7 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels else { // Parse and return non-streamed JSON response - var response = await ReadJsonAsync>(virtualConnection); + var response = await ReadJsonAsync>(virtualConnection, cancellationToken); if (response.ErrorMessage != null) { throw new NodeInvocationException(response.ErrorMessage, response.ErrorDetails); @@ -163,27 +168,27 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels base.Dispose(disposing); } - private static async Task WriteJsonLineAsync(Stream stream, object serializableObject) + private static async Task WriteJsonLineAsync(Stream stream, object serializableObject, CancellationToken cancellationToken) { var json = JsonConvert.SerializeObject(serializableObject, jsonSerializerSettings); var bytes = Encoding.UTF8.GetBytes(json + '\n'); - await stream.WriteAsync(bytes, 0, bytes.Length); + await stream.WriteAsync(bytes, 0, bytes.Length, cancellationToken); } - private static async Task ReadJsonAsync(Stream stream) + private static async Task ReadJsonAsync(Stream stream, CancellationToken cancellationToken) { - var json = Encoding.UTF8.GetString(await ReadAllBytesAsync(stream)); + var json = Encoding.UTF8.GetString(await ReadAllBytesAsync(stream, cancellationToken)); return JsonConvert.DeserializeObject(json, jsonSerializerSettings); } - private static async Task ReadAllBytesAsync(Stream input) + private static async Task ReadAllBytesAsync(Stream input, CancellationToken cancellationToken) { byte[] buffer = new byte[16 * 1024]; using (var ms = new MemoryStream()) { int read; - while ((read = await input.ReadAsync(buffer, 0, buffer.Length)) > 0) + while ((read = await input.ReadAsync(buffer, 0, buffer.Length, cancellationToken)) > 0) { ms.Write(buffer, 0, read); } diff --git a/src/Microsoft.AspNetCore.NodeServices/INodeServices.cs b/src/Microsoft.AspNetCore.NodeServices/INodeServices.cs index 3aa09e3..fbc32d0 100644 --- a/src/Microsoft.AspNetCore.NodeServices/INodeServices.cs +++ b/src/Microsoft.AspNetCore.NodeServices/INodeServices.cs @@ -1,4 +1,5 @@ using System; +using System.Threading; using System.Threading.Tasks; namespace Microsoft.AspNetCore.NodeServices @@ -6,8 +7,11 @@ namespace Microsoft.AspNetCore.NodeServices public interface INodeServices : IDisposable { Task InvokeAsync(string moduleName, params object[] args); + Task InvokeAsync(CancellationToken cancellationToken, string moduleName, params object[] args); Task InvokeExportAsync(string moduleName, string exportedFunctionName, params object[] args); + Task InvokeExportAsync(CancellationToken cancellationToken, string moduleName, string exportedFunctionName, params object[] args); + [Obsolete("Use InvokeAsync instead")] Task Invoke(string moduleName, params object[] args); diff --git a/src/Microsoft.AspNetCore.NodeServices/NodeServicesImpl.cs b/src/Microsoft.AspNetCore.NodeServices/NodeServicesImpl.cs index 037cc96..38d3055 100644 --- a/src/Microsoft.AspNetCore.NodeServices/NodeServicesImpl.cs +++ b/src/Microsoft.AspNetCore.NodeServices/NodeServicesImpl.cs @@ -1,4 +1,5 @@ using System; +using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.NodeServices.HostingModels; @@ -34,19 +35,29 @@ namespace Microsoft.AspNetCore.NodeServices return InvokeExportAsync(moduleName, null, args); } - public Task InvokeExportAsync(string moduleName, string exportedFunctionName, params object[] args) + public Task InvokeAsync(CancellationToken cancellationToken, string moduleName, params object[] args) { - return InvokeExportWithPossibleRetryAsync(moduleName, exportedFunctionName, args, allowRetry: true); + return InvokeExportAsync(cancellationToken, moduleName, null, args); } - public async Task InvokeExportWithPossibleRetryAsync(string moduleName, string exportedFunctionName, object[] args, bool allowRetry) + public Task InvokeExportAsync(string moduleName, string exportedFunctionName, params object[] args) + { + return InvokeExportWithPossibleRetryAsync(moduleName, exportedFunctionName, args, /* allowRetry */ true, CancellationToken.None); + } + + public Task InvokeExportAsync(CancellationToken cancellationToken, string moduleName, string exportedFunctionName, params object[] args) + { + return InvokeExportWithPossibleRetryAsync(moduleName, exportedFunctionName, args, /* allowRetry */ true, cancellationToken); + } + + public async Task InvokeExportWithPossibleRetryAsync(string moduleName, string exportedFunctionName, object[] args, bool allowRetry, CancellationToken cancellationToken) { ThrowAnyOutstandingDelayedDisposalException(); var nodeInstance = GetOrCreateCurrentNodeInstance(); try { - return await nodeInstance.InvokeExportAsync(moduleName, exportedFunctionName, args); + return await nodeInstance.InvokeExportAsync(cancellationToken, moduleName, exportedFunctionName, args); } catch (NodeInvocationException ex) { @@ -69,7 +80,7 @@ namespace Microsoft.AspNetCore.NodeServices // One the next call, don't allow retries, because we could get into an infinite retry loop, or a long retry // loop that masks an underlying problem. A newly-created Node instance should be able to accept invocations, // or something more serious must be wrong. - return await InvokeExportWithPossibleRetryAsync(moduleName, exportedFunctionName, args, allowRetry: false); + return await InvokeExportWithPossibleRetryAsync(moduleName, exportedFunctionName, args, /* allowRetry */ false, cancellationToken); } else { diff --git a/src/Microsoft.AspNetCore.NodeServices/Util/TaskExtensions.cs b/src/Microsoft.AspNetCore.NodeServices/Util/TaskExtensions.cs new file mode 100644 index 0000000..75cfdb1 --- /dev/null +++ b/src/Microsoft.AspNetCore.NodeServices/Util/TaskExtensions.cs @@ -0,0 +1,30 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.NodeServices +{ + internal static class TaskExtensions + { + public static Task OrThrowOnCancellation(this Task task, CancellationToken cancellationToken) + { + return task.IsCompleted + ? task // If the task is already completed, no need to wrap it in a further layer of task + : task.ContinueWith( + _ => {}, // If the task completes, allow execution to continue + cancellationToken, + TaskContinuationOptions.ExecuteSynchronously, + TaskScheduler.Default); + } + + public static Task OrThrowOnCancellation(this Task task, CancellationToken cancellationToken) + { + return task.IsCompleted + ? task // If the task is already completed, no need to wrap it in a further layer of task + : task.ContinueWith( + t => t.Result, // If the task completes, pass through its result + cancellationToken, + TaskContinuationOptions.ExecuteSynchronously, + TaskScheduler.Default); + } + } +} \ No newline at end of file From 041d173f56692a0b8e624a1db34c4560fe14e6df Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Thu, 8 Sep 2016 12:08:42 +0100 Subject: [PATCH 29/55] All NodeServices invocations now have a default timeout, plus a descriptive exception if that happens --- .../Configuration/NodeServicesFactory.cs | 4 +- .../Configuration/NodeServicesOptions.cs | 4 + .../HostingModels/HttpNodeInstance.cs | 4 +- .../HostingModels/OutOfProcessNodeInstance.cs | 80 ++++++++++++++++--- .../HostingModels/SocketNodeInstance.cs | 3 +- 5 files changed, 81 insertions(+), 14 deletions(-) diff --git a/src/Microsoft.AspNetCore.NodeServices/Configuration/NodeServicesFactory.cs b/src/Microsoft.AspNetCore.NodeServices/Configuration/NodeServicesFactory.cs index fc038f2..d6660c0 100644 --- a/src/Microsoft.AspNetCore.NodeServices/Configuration/NodeServicesFactory.cs +++ b/src/Microsoft.AspNetCore.NodeServices/Configuration/NodeServicesFactory.cs @@ -29,11 +29,11 @@ namespace Microsoft.AspNetCore.NodeServices { case NodeHostingModel.Http: return new HttpNodeInstance(options.ProjectPath, options.WatchFileExtensions, options.NodeInstanceOutputLogger, - options.EnvironmentVariables, options.LaunchWithDebugging, options.DebuggingPort, /* port */ 0); + options.EnvironmentVariables, options.InvocationTimeoutMilliseconds, options.LaunchWithDebugging, options.DebuggingPort, /* port */ 0); case NodeHostingModel.Socket: var pipeName = "pni-" + Guid.NewGuid().ToString("D"); // Arbitrary non-clashing string return new SocketNodeInstance(options.ProjectPath, options.WatchFileExtensions, pipeName, options.NodeInstanceOutputLogger, - options.EnvironmentVariables, options.LaunchWithDebugging, options.DebuggingPort); + options.EnvironmentVariables, options.InvocationTimeoutMilliseconds, options.LaunchWithDebugging, options.DebuggingPort); default: throw new ArgumentException("Unknown hosting model: " + options.HostingModel); } diff --git a/src/Microsoft.AspNetCore.NodeServices/Configuration/NodeServicesOptions.cs b/src/Microsoft.AspNetCore.NodeServices/Configuration/NodeServicesOptions.cs index edf9ae2..dbf1ce6 100644 --- a/src/Microsoft.AspNetCore.NodeServices/Configuration/NodeServicesOptions.cs +++ b/src/Microsoft.AspNetCore.NodeServices/Configuration/NodeServicesOptions.cs @@ -11,6 +11,8 @@ namespace Microsoft.AspNetCore.NodeServices public class NodeServicesOptions { public const NodeHostingModel DefaultNodeHostingModel = NodeHostingModel.Http; + internal const string TimeoutConfigPropertyName = nameof(InvocationTimeoutMilliseconds); + private const int DefaultInvocationTimeoutMilliseconds = 60 * 1000; private const string LogCategoryName = "Microsoft.AspNetCore.NodeServices"; private static readonly string[] DefaultWatchFileExtensions = { ".js", ".jsx", ".ts", ".tsx", ".json", ".html" }; @@ -22,6 +24,7 @@ namespace Microsoft.AspNetCore.NodeServices } EnvironmentVariables = new Dictionary(); + InvocationTimeoutMilliseconds = DefaultInvocationTimeoutMilliseconds; HostingModel = DefaultNodeHostingModel; WatchFileExtensions = (string[])DefaultWatchFileExtensions.Clone(); @@ -49,5 +52,6 @@ namespace Microsoft.AspNetCore.NodeServices public bool LaunchWithDebugging { get; set; } public IDictionary EnvironmentVariables { get; set; } public int DebuggingPort { get; set; } + public int InvocationTimeoutMilliseconds { get; set; } } } \ 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 0c11371..1ab4c13 100644 --- a/src/Microsoft.AspNetCore.NodeServices/HostingModels/HttpNodeInstance.cs +++ b/src/Microsoft.AspNetCore.NodeServices/HostingModels/HttpNodeInstance.cs @@ -37,7 +37,8 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels private int _portNumber; public HttpNodeInstance(string projectPath, string[] watchFileExtensions, ILogger nodeInstanceOutputLogger, - IDictionary environmentVars, bool launchWithDebugging, int debuggingPort, int port = 0) + IDictionary environmentVars, int invocationTimeoutMilliseconds, bool launchWithDebugging, + int debuggingPort, int port = 0) : base( EmbeddedResourceReader.Read( typeof(HttpNodeInstance), @@ -47,6 +48,7 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels MakeCommandLineOptions(port), nodeInstanceOutputLogger, environmentVars, + invocationTimeoutMilliseconds, launchWithDebugging, debuggingPort) { diff --git a/src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs b/src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs index 9a583d3..3957cb0 100644 --- a/src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs +++ b/src/Microsoft.AspNetCore.NodeServices/HostingModels/OutOfProcessNodeInstance.cs @@ -37,6 +37,7 @@ If you haven't yet installed node-inspector, you can do so as follows: private bool _disposed; private readonly StringAsTempFile _entryPointScript; private FileSystemWatcher _fileSystemWatcher; + private int _invocationTimeoutMilliseconds; private readonly Process _nodeProcess; private int? _nodeDebuggingPort; private bool _nodeProcessNeedsRestart; @@ -49,6 +50,7 @@ If you haven't yet installed node-inspector, you can do so as follows: string commandLineArguments, ILogger nodeOutputLogger, IDictionary environmentVars, + int invocationTimeoutMilliseconds, bool launchWithDebugging, int debuggingPort) { @@ -59,6 +61,7 @@ If you haven't yet installed node-inspector, you can do so as follows: OutputLogger = nodeOutputLogger; _entryPointScript = new StringAsTempFile(entryPointScript); + _invocationTimeoutMilliseconds = invocationTimeoutMilliseconds; var startInfo = PrepareNodeProcessStartInfo(_entryPointScript.FileName, projectPath, commandLineArguments, environmentVars, launchWithDebugging, debuggingPort); @@ -81,17 +84,74 @@ If you haven't yet installed node-inspector, you can do so as follows: throw new NodeInvocationException(message, null, nodeInstanceUnavailable: true); } - // Wait until the connection is established. This will throw if the connection fails to initialize, - // or if cancellation is requested first. Note that we can't really cancel the "establishing connection" - // task because that's shared with all callers, but we can stop waiting for it if this call is cancelled. - await _connectionIsReadySource.Task.OrThrowOnCancellation(cancellationToken); - - return await InvokeExportAsync(new NodeInvocationInfo + // Construct a new cancellation token that combines the supplied token with the configured invocation + // timeout. Technically we could avoid wrapping the cancellationToken if no timeout is configured, + // but that's not really a major use case, since timeouts are enabled by default. + using (var timeoutSource = new CancellationTokenSource()) + using (var combinedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutSource.Token)) { - ModuleName = moduleName, - ExportedFunctionName = exportNameOrNull, - Args = args - }, cancellationToken); + if (_invocationTimeoutMilliseconds > 0) + { + timeoutSource.CancelAfter(_invocationTimeoutMilliseconds); + } + + // By overwriting the supplied cancellation token, we ensure that it isn't accidentally used + // below. We only want to pass through the token that respects timeouts. + cancellationToken = combinedCancellationTokenSource.Token; + var connectionDidSucceed = false; + + try + { + // Wait until the connection is established. This will throw if the connection fails to initialize, + // or if cancellation is requested first. Note that we can't really cancel the "establishing connection" + // task because that's shared with all callers, but we can stop waiting for it if this call is cancelled. + await _connectionIsReadySource.Task.OrThrowOnCancellation(cancellationToken); + connectionDidSucceed = true; + + return await InvokeExportAsync(new NodeInvocationInfo + { + ModuleName = moduleName, + ExportedFunctionName = exportNameOrNull, + Args = args + }, cancellationToken); + } + catch (TaskCanceledException) + { + if (timeoutSource.IsCancellationRequested) + { + // It was very common for developers to report 'TaskCanceledException' when encountering almost any + // trouble when using NodeServices. Now we have a default invocation timeout, and attempt to give + // a more descriptive exception message if it happens. + if (!connectionDidSucceed) + { + // This is very unlikely, but for debugging, it's still useful to differentiate it from the + // case below. + throw new NodeInvocationException( + $"Attempt to connect to Node timed out after {_invocationTimeoutMilliseconds}ms.", + string.Empty); + } + else + { + // Developers encounter this fairly often (if their Node code fails without invoking the callback, + // all that the .NET side knows is that the invocation eventually times out). Previously, this surfaced + // as a TaskCanceledException, but this led to a lot of issue reports. Now we throw the following + // descriptive error. + throw new NodeInvocationException( + $"The Node invocation timed out after {_invocationTimeoutMilliseconds}ms.", + $"You can change the timeout duration by setting the {NodeServicesOptions.TimeoutConfigPropertyName} " + + $"property on {nameof(NodeServicesOptions)}.\n\n" + + "The first debugging step is to ensure that your Node.js function always invokes the supplied " + + "callback (or throws an exception synchronously), even if it encounters an error. Otherwise, " + + "the .NET code has no way to know that it is finished or has failed." + ); + } + } + else + { + throw; + } + } + } } public void Dispose() diff --git a/src/Microsoft.AspNetCore.NodeServices/HostingModels/SocketNodeInstance.cs b/src/Microsoft.AspNetCore.NodeServices/HostingModels/SocketNodeInstance.cs index 2acfc0e..2ee2aa4 100644 --- a/src/Microsoft.AspNetCore.NodeServices/HostingModels/SocketNodeInstance.cs +++ b/src/Microsoft.AspNetCore.NodeServices/HostingModels/SocketNodeInstance.cs @@ -41,7 +41,7 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels public SocketNodeInstance(string projectPath, string[] watchFileExtensions, string socketAddress, ILogger nodeInstanceOutputLogger, IDictionary environmentVars, - bool launchWithDebugging, int debuggingPort) + int invocationTimeoutMilliseconds, bool launchWithDebugging, int debuggingPort) : base( EmbeddedResourceReader.Read( typeof(SocketNodeInstance), @@ -51,6 +51,7 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels MakeNewCommandLineOptions(socketAddress), nodeInstanceOutputLogger, environmentVars, + invocationTimeoutMilliseconds, launchWithDebugging, debuggingPort) { From 411100478a31bb9640afb339343575bed8184fc6 Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Thu, 8 Sep 2016 12:14:45 +0100 Subject: [PATCH 30/55] Fix double-encoding typo --- .../Content/Node/entrypoint-http.js | 2 +- .../TypeScript/HttpNodeInstanceEntryPoint.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.AspNetCore.NodeServices/Content/Node/entrypoint-http.js b/src/Microsoft.AspNetCore.NodeServices/Content/Node/entrypoint-http.js index 9d231f3..cfa15bc 100644 --- a/src/Microsoft.AspNetCore.NodeServices/Content/Node/entrypoint-http.js +++ b/src/Microsoft.AspNetCore.NodeServices/Content/Node/entrypoint-http.js @@ -83,7 +83,7 @@ return; } res.setHeader('Content-Type', 'application/json'); - res.end(JSON.stringify(successValueJson)); + res.end(successValueJson); } else { // String - can bypass JSON-serialization altogether diff --git a/src/Microsoft.AspNetCore.NodeServices/TypeScript/HttpNodeInstanceEntryPoint.ts b/src/Microsoft.AspNetCore.NodeServices/TypeScript/HttpNodeInstanceEntryPoint.ts index 5bb8623..f8e619d 100644 --- a/src/Microsoft.AspNetCore.NodeServices/TypeScript/HttpNodeInstanceEntryPoint.ts +++ b/src/Microsoft.AspNetCore.NodeServices/TypeScript/HttpNodeInstanceEntryPoint.ts @@ -29,7 +29,7 @@ const server = http.createServer((req, res) => { return; } res.setHeader('Content-Type', 'application/json'); - res.end(JSON.stringify(successValueJson)); + res.end(successValueJson); } else { // String - can bypass JSON-serialization altogether res.setHeader('Content-Type', 'text/plain'); From 4ca1669db130970b7ce9f05d8e71de4e28e5a4e0 Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Thu, 8 Sep 2016 12:56:05 +0100 Subject: [PATCH 31/55] Prerendering imposes its own (overridable) timeout with descriptive error --- .../Prerendering/PrerenderTagHelper.cs | 7 +++- .../Prerendering/Prerenderer.cs | 6 ++- .../aspnet-prerendering/src/Prerendering.ts | 39 ++++++++++++++++++- 3 files changed, 47 insertions(+), 5 deletions(-) diff --git a/src/Microsoft.AspNetCore.SpaServices/Prerendering/PrerenderTagHelper.cs b/src/Microsoft.AspNetCore.SpaServices/Prerendering/PrerenderTagHelper.cs index b91c2a6..de02c90 100644 --- a/src/Microsoft.AspNetCore.SpaServices/Prerendering/PrerenderTagHelper.cs +++ b/src/Microsoft.AspNetCore.SpaServices/Prerendering/PrerenderTagHelper.cs @@ -18,6 +18,7 @@ namespace Microsoft.AspNetCore.SpaServices.Prerendering private const string PrerenderExportAttributeName = "asp-prerender-export"; private const string PrerenderWebpackConfigAttributeName = "asp-prerender-webpack-config"; private const string PrerenderDataAttributeName = "asp-prerender-data"; + private const string PrerenderTimeoutAttributeName = "asp-prerender-timeout"; private static INodeServices _fallbackNodeServices; // Used only if no INodeServices was registered with DI private readonly string _applicationBasePath; @@ -50,6 +51,9 @@ namespace Microsoft.AspNetCore.SpaServices.Prerendering [HtmlAttributeName(PrerenderDataAttributeName)] public object CustomDataParameter { get; set; } + [HtmlAttributeName(PrerenderTimeoutAttributeName)] + public int TimeoutMillisecondsParameter { get; set; } + [HtmlAttributeNotBound] [ViewContext] public ViewContext ViewContext { get; set; } @@ -79,7 +83,8 @@ namespace Microsoft.AspNetCore.SpaServices.Prerendering }, unencodedAbsoluteUrl, unencodedPathAndQuery, - CustomDataParameter); + CustomDataParameter, + TimeoutMillisecondsParameter); output.Content.SetHtmlContent(result.Html); // Also attach any specified globals to the 'window' object. This is useful for transferring diff --git a/src/Microsoft.AspNetCore.SpaServices/Prerendering/Prerenderer.cs b/src/Microsoft.AspNetCore.SpaServices/Prerendering/Prerenderer.cs index 40286d1..9d8724e 100644 --- a/src/Microsoft.AspNetCore.SpaServices/Prerendering/Prerenderer.cs +++ b/src/Microsoft.AspNetCore.SpaServices/Prerendering/Prerenderer.cs @@ -23,7 +23,8 @@ namespace Microsoft.AspNetCore.SpaServices.Prerendering JavaScriptModuleExport bootModule, string requestAbsoluteUrl, string requestPathAndQuery, - object customDataParameter) + object customDataParameter, + int timeoutMilliseconds) { return nodeServices.InvokeExportAsync( NodeScript.Value.FileName, @@ -32,7 +33,8 @@ namespace Microsoft.AspNetCore.SpaServices.Prerendering bootModule, requestAbsoluteUrl, requestPathAndQuery, - customDataParameter); + customDataParameter, + timeoutMilliseconds); } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-prerendering/src/Prerendering.ts b/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-prerendering/src/Prerendering.ts index 4d9269b..3a14f94 100644 --- a/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-prerendering/src/Prerendering.ts +++ b/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-prerendering/src/Prerendering.ts @@ -5,6 +5,8 @@ import * as domain from 'domain'; import { run as domainTaskRun } from 'domain-task/main'; import { baseUrl } from 'domain-task/fetch'; +const defaultTimeoutMilliseconds = 30 * 1000; + export interface RenderToStringCallback { (error: any, result: RenderToStringResult): void; } @@ -33,7 +35,7 @@ export interface BootModuleInfo { webpackConfig?: string; } -export function renderToString(callback: RenderToStringCallback, applicationBasePath: string, bootModule: BootModuleInfo, absoluteRequestUrl: string, requestPathAndQuery: string, customDataParameter: any) { +export function renderToString(callback: RenderToStringCallback, applicationBasePath: string, bootModule: BootModuleInfo, absoluteRequestUrl: string, requestPathAndQuery: string, customDataParameter: any, overrideTimeoutMilliseconds: number) { findBootFunc(applicationBasePath, bootModule, (findBootFuncError, bootFunc) => { if (findBootFuncError) { callback(findBootFuncError, null); @@ -66,8 +68,22 @@ export function renderToString(callback: RenderToStringCallback, applicationBase // Make the base URL available to the 'domain-tasks/fetch' helper within this execution context baseUrl(absoluteRequestUrl); + // Begin rendering, and apply a timeout + const bootFuncPromise = bootFunc(params); + if (!bootFuncPromise || typeof bootFuncPromise.then !== 'function') { + callback(`Prerendering failed because the boot function in ${bootModule.moduleName} did not return a promise.`, null); + return; + } + const timeoutMilliseconds = overrideTimeoutMilliseconds || defaultTimeoutMilliseconds; // e.g., pass -1 to override as 'never time out' + const bootFuncPromiseWithTimeout = timeoutMilliseconds > 0 + ? wrapWithTimeout(bootFuncPromise, timeoutMilliseconds, + `Prerendering timed out after ${timeoutMilliseconds}ms because the boot function in '${bootModule.moduleName}' ` + + 'returned a promise that did not resolve or reject. Make sure that your boot function always resolves or ' + + 'rejects its promise. You can change the timeout value using the \'asp-prerender-timeout\' tag helper.') + : bootFuncPromise; + // Actually perform the rendering - bootFunc(params).then(successResult => { + bootFuncPromiseWithTimeout.then(successResult => { callback(null, { html: successResult.html, globals: successResult.globals }); }, error => { callback(error, null); @@ -84,6 +100,25 @@ export function renderToString(callback: RenderToStringCallback, applicationBase }); } +function wrapWithTimeout(promise: Promise, timeoutMilliseconds: number, timeoutRejectionValue: any): Promise { + return new Promise((resolve, reject) => { + const timeoutTimer = setTimeout(() => { + reject(timeoutRejectionValue); + }, timeoutMilliseconds); + + promise.then( + resolvedValue => { + clearTimeout(timeoutTimer); + resolve(resolvedValue); + }, + rejectedValue => { + clearTimeout(timeoutTimer); + reject(rejectedValue); + } + ) + }); +} + function findBootModule(applicationBasePath: string, bootModule: BootModuleInfo, callback: (error: any, foundModule: T) => void) { const bootModuleNameFullPath = path.resolve(applicationBasePath, bootModule.moduleName); if (bootModule.webpackConfig) { From 1f2168949d54ea3cdb7573fecff7a7dc0932dbfc Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Thu, 8 Sep 2016 12:56:46 +0100 Subject: [PATCH 32/55] Publish updated aspnet-prerendering NPM package --- .../npm/aspnet-prerendering/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-prerendering/package.json b/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-prerendering/package.json index 156a6f1..4fc59a5 100644 --- a/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-prerendering/package.json +++ b/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-prerendering/package.json @@ -1,6 +1,6 @@ { "name": "aspnet-prerendering", - "version": "1.0.4", + "version": "1.0.5", "description": "Helpers for server-side rendering of JavaScript applications in ASP.NET Core projects. Works in conjunction with the Microsoft.AspNetCore.SpaServices NuGet package.", "main": "index.js", "scripts": { From 5fcce843bad20262d91bf9be31544382e17eb920 Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Thu, 8 Sep 2016 14:32:25 +0100 Subject: [PATCH 33/55] Workaround to fix #235 (add placeholder inside node_modules) --- .gitignore | 9 ++++++++- templates/Angular2Spa/node_modules/_placeholder.txt | 7 +++++++ templates/Angular2Spa/template_gitignore | 5 ++++- templates/KnockoutSpa/node_modules/_placeholder.txt | 7 +++++++ templates/KnockoutSpa/template_gitignore | 5 ++++- templates/ReactReduxSpa/node_modules/_placeholder.txt | 7 +++++++ templates/ReactReduxSpa/template_gitignore | 5 ++++- templates/ReactSpa/node_modules/_placeholder.txt | 7 +++++++ templates/ReactSpa/template_gitignore | 5 ++++- .../WebApplicationBasic/node_modules/_placeholder.txt | 7 +++++++ templates/WebApplicationBasic/template_gitignore | 5 ++++- 11 files changed, 63 insertions(+), 6 deletions(-) create mode 100644 templates/Angular2Spa/node_modules/_placeholder.txt create mode 100644 templates/KnockoutSpa/node_modules/_placeholder.txt create mode 100644 templates/ReactReduxSpa/node_modules/_placeholder.txt create mode 100644 templates/ReactSpa/node_modules/_placeholder.txt create mode 100644 templates/WebApplicationBasic/node_modules/_placeholder.txt diff --git a/.gitignore b/.gitignore index 06cc11a..6df4b42 100644 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,13 @@ npm-debug.log # files with that name (https://github.com/npm/npm/issues/1862). So, each template instead has a template_gitignore # file which gets renamed after the files are copied. And so any files that need to be excluded in the source # repo have to be excluded here. -/templates/*/node_modules/ + +# Note that we need to exclude node_modules/** (i.e., subdirs, not the whole of node_modules) because we do need to +# include the _placeholder.txt files, and can't do that using gitignore exclusion because developers aren't promoted to +# commit files included that way. This is all a workaround for Workaround for https://github.com/aspnet/JavaScriptServices/issues/235 +/templates/*/node_modules/** /templates/*/wwwroot/dist/ .vscode/ + +# Workaround for https://github.com/aspnet/JavaScriptServices/issues/235 +!/templates/*/node_modules/_placeholder.txt diff --git a/templates/Angular2Spa/node_modules/_placeholder.txt b/templates/Angular2Spa/node_modules/_placeholder.txt new file mode 100644 index 0000000..09ae8c0 --- /dev/null +++ b/templates/Angular2Spa/node_modules/_placeholder.txt @@ -0,0 +1,7 @@ +This file exists as a workaround for https://github.com/dotnet/cli/issues/1396 +('dotnet publish' does not publish any directories that didn't exist or were +empty before the publish script started, which means it's not enough just to +run 'npm install' during publishing: you need to ensure node_modules already +existed at least). + +Hopefully, this can be removed after the move to the new MSBuild. diff --git a/templates/Angular2Spa/template_gitignore b/templates/Angular2Spa/template_gitignore index d410b8b..2ee8e59 100644 --- a/templates/Angular2Spa/template_gitignore +++ b/templates/Angular2Spa/template_gitignore @@ -184,9 +184,12 @@ ClientBin/ *.dbproj.schemaview *.pfx *.publishsettings -node_modules/ orleans.codegen.cs +# Workaround for https://github.com/aspnet/JavaScriptServices/issues/235 +/node_modules/** +!/node_modules/_placeholder.txt + # RIA/Silverlight projects Generated_Code/ diff --git a/templates/KnockoutSpa/node_modules/_placeholder.txt b/templates/KnockoutSpa/node_modules/_placeholder.txt new file mode 100644 index 0000000..09ae8c0 --- /dev/null +++ b/templates/KnockoutSpa/node_modules/_placeholder.txt @@ -0,0 +1,7 @@ +This file exists as a workaround for https://github.com/dotnet/cli/issues/1396 +('dotnet publish' does not publish any directories that didn't exist or were +empty before the publish script started, which means it's not enough just to +run 'npm install' during publishing: you need to ensure node_modules already +existed at least). + +Hopefully, this can be removed after the move to the new MSBuild. diff --git a/templates/KnockoutSpa/template_gitignore b/templates/KnockoutSpa/template_gitignore index d410b8b..2ee8e59 100644 --- a/templates/KnockoutSpa/template_gitignore +++ b/templates/KnockoutSpa/template_gitignore @@ -184,9 +184,12 @@ ClientBin/ *.dbproj.schemaview *.pfx *.publishsettings -node_modules/ orleans.codegen.cs +# Workaround for https://github.com/aspnet/JavaScriptServices/issues/235 +/node_modules/** +!/node_modules/_placeholder.txt + # RIA/Silverlight projects Generated_Code/ diff --git a/templates/ReactReduxSpa/node_modules/_placeholder.txt b/templates/ReactReduxSpa/node_modules/_placeholder.txt new file mode 100644 index 0000000..09ae8c0 --- /dev/null +++ b/templates/ReactReduxSpa/node_modules/_placeholder.txt @@ -0,0 +1,7 @@ +This file exists as a workaround for https://github.com/dotnet/cli/issues/1396 +('dotnet publish' does not publish any directories that didn't exist or were +empty before the publish script started, which means it's not enough just to +run 'npm install' during publishing: you need to ensure node_modules already +existed at least). + +Hopefully, this can be removed after the move to the new MSBuild. diff --git a/templates/ReactReduxSpa/template_gitignore b/templates/ReactReduxSpa/template_gitignore index d410b8b..2ee8e59 100644 --- a/templates/ReactReduxSpa/template_gitignore +++ b/templates/ReactReduxSpa/template_gitignore @@ -184,9 +184,12 @@ ClientBin/ *.dbproj.schemaview *.pfx *.publishsettings -node_modules/ orleans.codegen.cs +# Workaround for https://github.com/aspnet/JavaScriptServices/issues/235 +/node_modules/** +!/node_modules/_placeholder.txt + # RIA/Silverlight projects Generated_Code/ diff --git a/templates/ReactSpa/node_modules/_placeholder.txt b/templates/ReactSpa/node_modules/_placeholder.txt new file mode 100644 index 0000000..09ae8c0 --- /dev/null +++ b/templates/ReactSpa/node_modules/_placeholder.txt @@ -0,0 +1,7 @@ +This file exists as a workaround for https://github.com/dotnet/cli/issues/1396 +('dotnet publish' does not publish any directories that didn't exist or were +empty before the publish script started, which means it's not enough just to +run 'npm install' during publishing: you need to ensure node_modules already +existed at least). + +Hopefully, this can be removed after the move to the new MSBuild. diff --git a/templates/ReactSpa/template_gitignore b/templates/ReactSpa/template_gitignore index d410b8b..2ee8e59 100644 --- a/templates/ReactSpa/template_gitignore +++ b/templates/ReactSpa/template_gitignore @@ -184,9 +184,12 @@ ClientBin/ *.dbproj.schemaview *.pfx *.publishsettings -node_modules/ orleans.codegen.cs +# Workaround for https://github.com/aspnet/JavaScriptServices/issues/235 +/node_modules/** +!/node_modules/_placeholder.txt + # RIA/Silverlight projects Generated_Code/ diff --git a/templates/WebApplicationBasic/node_modules/_placeholder.txt b/templates/WebApplicationBasic/node_modules/_placeholder.txt new file mode 100644 index 0000000..09ae8c0 --- /dev/null +++ b/templates/WebApplicationBasic/node_modules/_placeholder.txt @@ -0,0 +1,7 @@ +This file exists as a workaround for https://github.com/dotnet/cli/issues/1396 +('dotnet publish' does not publish any directories that didn't exist or were +empty before the publish script started, which means it's not enough just to +run 'npm install' during publishing: you need to ensure node_modules already +existed at least). + +Hopefully, this can be removed after the move to the new MSBuild. diff --git a/templates/WebApplicationBasic/template_gitignore b/templates/WebApplicationBasic/template_gitignore index d410b8b..2ee8e59 100644 --- a/templates/WebApplicationBasic/template_gitignore +++ b/templates/WebApplicationBasic/template_gitignore @@ -184,9 +184,12 @@ ClientBin/ *.dbproj.schemaview *.pfx *.publishsettings -node_modules/ orleans.codegen.cs +# Workaround for https://github.com/aspnet/JavaScriptServices/issues/235 +/node_modules/** +!/node_modules/_placeholder.txt + # RIA/Silverlight projects Generated_Code/ From 5750c4aab70749882e0c8fb99fa315835315e00a Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Thu, 8 Sep 2016 14:44:37 +0100 Subject: [PATCH 34/55] Publish updated generator-aspnetcore-spa package (0.2.5) --- templates/package-builder/src/yeoman/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/package-builder/src/yeoman/package.json b/templates/package-builder/src/yeoman/package.json index 8202518..f68b9ae 100644 --- a/templates/package-builder/src/yeoman/package.json +++ b/templates/package-builder/src/yeoman/package.json @@ -1,6 +1,6 @@ { "name": "generator-aspnetcore-spa", - "version": "0.2.4", + "version": "0.2.5", "description": "Single-Page App templates for ASP.NET Core", "author": "Microsoft", "license": "Apache-2.0", From 0bcf4b0700c7bf4cb1de6b4124f0ee87427351b5 Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Thu, 8 Sep 2016 15:58:47 +0100 Subject: [PATCH 35/55] Further work on fix for #235 (solving additional problem that 'npm publish' is hardcoded to exclude node_modules dirs) --- .gitignore | 9 +-------- ...lder.txt => template_nodemodules_placeholder.txt} | 0 ...lder.txt => template_nodemodules_placeholder.txt} | 0 ...lder.txt => template_nodemodules_placeholder.txt} | 0 ...lder.txt => template_nodemodules_placeholder.txt} | 0 ...lder.txt => template_nodemodules_placeholder.txt} | 0 templates/package-builder/src/build/build.ts | 6 +++++- templates/package-builder/src/yeoman/app/index.ts | 12 ++++++++++++ templates/package-builder/src/yeoman/package.json | 2 +- 9 files changed, 19 insertions(+), 10 deletions(-) rename templates/Angular2Spa/{node_modules/_placeholder.txt => template_nodemodules_placeholder.txt} (100%) rename templates/KnockoutSpa/{node_modules/_placeholder.txt => template_nodemodules_placeholder.txt} (100%) rename templates/ReactReduxSpa/{node_modules/_placeholder.txt => template_nodemodules_placeholder.txt} (100%) rename templates/ReactSpa/{node_modules/_placeholder.txt => template_nodemodules_placeholder.txt} (100%) rename templates/WebApplicationBasic/{node_modules/_placeholder.txt => template_nodemodules_placeholder.txt} (100%) diff --git a/.gitignore b/.gitignore index 6df4b42..06cc11a 100644 --- a/.gitignore +++ b/.gitignore @@ -34,13 +34,6 @@ npm-debug.log # files with that name (https://github.com/npm/npm/issues/1862). So, each template instead has a template_gitignore # file which gets renamed after the files are copied. And so any files that need to be excluded in the source # repo have to be excluded here. - -# Note that we need to exclude node_modules/** (i.e., subdirs, not the whole of node_modules) because we do need to -# include the _placeholder.txt files, and can't do that using gitignore exclusion because developers aren't promoted to -# commit files included that way. This is all a workaround for Workaround for https://github.com/aspnet/JavaScriptServices/issues/235 -/templates/*/node_modules/** +/templates/*/node_modules/ /templates/*/wwwroot/dist/ .vscode/ - -# Workaround for https://github.com/aspnet/JavaScriptServices/issues/235 -!/templates/*/node_modules/_placeholder.txt diff --git a/templates/Angular2Spa/node_modules/_placeholder.txt b/templates/Angular2Spa/template_nodemodules_placeholder.txt similarity index 100% rename from templates/Angular2Spa/node_modules/_placeholder.txt rename to templates/Angular2Spa/template_nodemodules_placeholder.txt diff --git a/templates/KnockoutSpa/node_modules/_placeholder.txt b/templates/KnockoutSpa/template_nodemodules_placeholder.txt similarity index 100% rename from templates/KnockoutSpa/node_modules/_placeholder.txt rename to templates/KnockoutSpa/template_nodemodules_placeholder.txt diff --git a/templates/ReactReduxSpa/node_modules/_placeholder.txt b/templates/ReactReduxSpa/template_nodemodules_placeholder.txt similarity index 100% rename from templates/ReactReduxSpa/node_modules/_placeholder.txt rename to templates/ReactReduxSpa/template_nodemodules_placeholder.txt diff --git a/templates/ReactSpa/node_modules/_placeholder.txt b/templates/ReactSpa/template_nodemodules_placeholder.txt similarity index 100% rename from templates/ReactSpa/node_modules/_placeholder.txt rename to templates/ReactSpa/template_nodemodules_placeholder.txt diff --git a/templates/WebApplicationBasic/node_modules/_placeholder.txt b/templates/WebApplicationBasic/template_nodemodules_placeholder.txt similarity index 100% rename from templates/WebApplicationBasic/node_modules/_placeholder.txt rename to templates/WebApplicationBasic/template_nodemodules_placeholder.txt diff --git a/templates/package-builder/src/build/build.ts b/templates/package-builder/src/build/build.ts index 546ff89..a0433bd 100644 --- a/templates/package-builder/src/build/build.ts +++ b/templates/package-builder/src/build/build.ts @@ -110,7 +110,11 @@ function buildDotNetNewNuGetPackage() { const projectGuid = '00000000-0000-0000-0000-000000000000'; const filenameReplacements = [ { from: /.*\.xproj$/, to: `${sourceProjectName}.xproj` }, - { from: /\btemplate_gitignore$/, to: '.gitignore' } + { from: /\btemplate_gitignore$/, to: '.gitignore' }, + + // Workaround for https://github.com/aspnet/JavaScriptServices/issues/235 + // For details, see the comment in ../yeoman/app/index.ts + { from: /\btemplate_nodemodules_placeholder.txt$/, to: 'node_modules/_placeholder.txt' } ]; const contentReplacements = [ { from: /[0-9a-f\-]{36}<\/ProjectGuid>/g, to: `${projectGuid}` }, diff --git a/templates/package-builder/src/yeoman/app/index.ts b/templates/package-builder/src/yeoman/app/index.ts index ecb5366..04687a0 100644 --- a/templates/package-builder/src/yeoman/app/index.ts +++ b/templates/package-builder/src/yeoman/app/index.ts @@ -52,6 +52,18 @@ class MyGenerator extends yeoman.Base { outputFn = path.join(path.dirname(fn), '.gitignore'); } + // Likewise, output template_nodemodules_placeholder.txt as node_modules/_placeholder.txt + // This is a workaround for https://github.com/aspnet/JavaScriptServices/issues/235. We need the new project + // to have a nonempty node_modules dir as far as *source control* is concerned. So, there's a gitignore + // rule that explicitly causes node_modules/_placeholder.txt to be tracked in source control. But how + // does that file get there in the first place? It's not enough for such a file to exist when the + // generator-aspnetcore-spa NPM package is published, because NPM doesn't allow any directories called + // node_modules to exist in the package. So we have a file with at a different location, and move it + // to node_modules as part of executing the template. + if (path.basename(fn) === 'template_nodemodules_placeholder.txt') { + outputFn = path.join(path.dirname(fn), 'node_modules', '_placeholder.txt'); + } + this.fs.copyTpl( path.join(templateRoot, fn), this.destinationPath(outputFn), diff --git a/templates/package-builder/src/yeoman/package.json b/templates/package-builder/src/yeoman/package.json index f68b9ae..49302f9 100644 --- a/templates/package-builder/src/yeoman/package.json +++ b/templates/package-builder/src/yeoman/package.json @@ -1,6 +1,6 @@ { "name": "generator-aspnetcore-spa", - "version": "0.2.5", + "version": "0.2.6", "description": "Single-Page App templates for ASP.NET Core", "author": "Microsoft", "license": "Apache-2.0", From 874575ba92ee4438a9510141c989b298324c6ec3 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Fri, 9 Sep 2016 09:44:16 +0100 Subject: [PATCH 36/55] Fix instructions for running samples. Fixes #301 --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 287a5fe..09881eb 100644 --- a/README.md +++ b/README.md @@ -69,8 +69,9 @@ Also in this repo, [the `samples` directory](https://github.com/aspnet/JavaScrip **To run the samples:** * Clone this repo - * Change directory to the same you want to run (e.g., `cd samples/angular/MusicStore`) - * Restore dependencies (run `dotnet restore` and `npm install`). + * At the repo's root directory (the one containing `src`, `samples`, etc.), run `dotnet restore` + * Change directory to the sample you want to run (e.g., `cd samples/angular/MusicStore`) + * Restore Node dependencies by running `npm install` * If you're trying to run the Angular 2 "Music Store" sample, then also run `gulp` (which you need to have installed globally). None of the other samples require this. * Run the application (`dotnet run`) * Browse to [http://localhost:5000](http://localhost:5000) From c2c45b04df4ecff721c407ff6fc57d082fe92712 Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Fri, 9 Sep 2016 11:06:36 +0100 Subject: [PATCH 37/55] In preparation for supporting redirections, aspnet-prerendering now passes through all boot func resolution props to .NET code --- .../npm/aspnet-prerendering/package.json | 2 +- .../npm/aspnet-prerendering/src/Prerendering.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-prerendering/package.json b/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-prerendering/package.json index 4fc59a5..5c188eb 100644 --- a/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-prerendering/package.json +++ b/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-prerendering/package.json @@ -1,6 +1,6 @@ { "name": "aspnet-prerendering", - "version": "1.0.5", + "version": "1.0.6", "description": "Helpers for server-side rendering of JavaScript applications in ASP.NET Core projects. Works in conjunction with the Microsoft.AspNetCore.SpaServices NuGet package.", "main": "index.js", "scripts": { diff --git a/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-prerendering/src/Prerendering.ts b/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-prerendering/src/Prerendering.ts index 3a14f94..6740cc9 100644 --- a/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-prerendering/src/Prerendering.ts +++ b/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-prerendering/src/Prerendering.ts @@ -84,7 +84,7 @@ export function renderToString(callback: RenderToStringCallback, applicationBase // Actually perform the rendering bootFuncPromiseWithTimeout.then(successResult => { - callback(null, { html: successResult.html, globals: successResult.globals }); + callback(null, successResult); }, error => { callback(error, null); }); From 1be9102aea7f0746a9b1219191c2abad70804f49 Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Fri, 9 Sep 2016 11:09:44 +0100 Subject: [PATCH 38/55] Prerendering server-side code can now issue redirections. Fixes #280 --- .../Prerendering/PrerenderTagHelper.cs | 9 +++++++++ .../Prerendering/RenderToStringResult.cs | 1 + 2 files changed, 10 insertions(+) diff --git a/src/Microsoft.AspNetCore.SpaServices/Prerendering/PrerenderTagHelper.cs b/src/Microsoft.AspNetCore.SpaServices/Prerendering/PrerenderTagHelper.cs index de02c90..7bce95c 100644 --- a/src/Microsoft.AspNetCore.SpaServices/Prerendering/PrerenderTagHelper.cs +++ b/src/Microsoft.AspNetCore.SpaServices/Prerendering/PrerenderTagHelper.cs @@ -85,6 +85,15 @@ namespace Microsoft.AspNetCore.SpaServices.Prerendering unencodedPathAndQuery, CustomDataParameter, TimeoutMillisecondsParameter); + + if (!string.IsNullOrEmpty(result.RedirectUrl)) + { + // It's a redirection + ViewContext.HttpContext.Response.Redirect(result.RedirectUrl); + return; + } + + // It's some HTML to inject output.Content.SetHtmlContent(result.Html); // Also attach any specified globals to the 'window' object. This is useful for transferring diff --git a/src/Microsoft.AspNetCore.SpaServices/Prerendering/RenderToStringResult.cs b/src/Microsoft.AspNetCore.SpaServices/Prerendering/RenderToStringResult.cs index 1d5b482..cae8d63 100644 --- a/src/Microsoft.AspNetCore.SpaServices/Prerendering/RenderToStringResult.cs +++ b/src/Microsoft.AspNetCore.SpaServices/Prerendering/RenderToStringResult.cs @@ -6,5 +6,6 @@ namespace Microsoft.AspNetCore.SpaServices.Prerendering { public JObject Globals { get; set; } public string Html { get; set; } + public string RedirectUrl { get; set; } } } \ No newline at end of file From 28550784edb6c9c453a8983285dc0d6a9aaa12a9 Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Fri, 9 Sep 2016 11:12:59 +0100 Subject: [PATCH 39/55] ReactReduxSpa's boot-server now supports redirections issued by react-router --- templates/ReactReduxSpa/ClientApp/boot-server.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/templates/ReactReduxSpa/ClientApp/boot-server.tsx b/templates/ReactReduxSpa/ClientApp/boot-server.tsx index fa3289e..62aefcf 100644 --- a/templates/ReactReduxSpa/ClientApp/boot-server.tsx +++ b/templates/ReactReduxSpa/ClientApp/boot-server.tsx @@ -5,15 +5,22 @@ import { match, RouterContext } from 'react-router'; import createMemoryHistory from 'history/lib/createMemoryHistory'; import routes from './routes'; import configureStore from './configureStore'; +type BootResult = { html?: string, globals?: { [key: string]: any }, redirectUrl?: string}; export default function (params: any): Promise<{ html: string }> { - return new Promise<{ html: string, globals: { [key: string]: any } }>((resolve, reject) => { + return new Promise((resolve, reject) => { // Match the incoming request against the list of client-side routes match({ routes, location: params.location }, (error, redirectLocation, renderProps: any) => { if (error) { throw error; } + // If there's a redirection, just send this information back to the host application + if (redirectLocation) { + resolve({ redirectUrl: redirectLocation.pathname }); + return; + } + // If it didn't match any route, renderProps will be undefined if (!renderProps) { throw new Error(`The location '${ params.url }' doesn't match any route configured in react-router.`); From b4bec30b0f1610f759bb5dd369a24bd26484ba86 Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Fri, 9 Sep 2016 11:31:36 +0100 Subject: [PATCH 40/55] Clean up dependencies vs devDependencies in templates --- templates/Angular2Spa/package.json | 32 +++++++++++++--------------- templates/KnockoutSpa/package.json | 4 +--- templates/ReactReduxSpa/package.json | 24 ++++++++++----------- templates/ReactSpa/package.json | 12 +++++------ 4 files changed, 32 insertions(+), 40 deletions(-) diff --git a/templates/Angular2Spa/package.json b/templates/Angular2Spa/package.json index 3797004..04fab27 100644 --- a/templates/Angular2Spa/package.json +++ b/templates/Angular2Spa/package.json @@ -1,23 +1,6 @@ { "name": "WebApplicationBasic", "version": "0.0.0", - "devDependencies": { - "aspnet-webpack": "^1.0.6", - "bootstrap": "^3.3.6", - "css-loader": "^0.23.1", - "expose-loader": "^0.7.1", - "extendify": "^1.0.0", - "extract-text-webpack-plugin": "^1.0.1", - "file-loader": "^0.8.5", - "jquery": "^2.2.1", - "raw-loader": "^0.5.1", - "style-loader": "^0.13.0", - "ts-loader": "^0.8.1", - "typescript": "^1.8.2", - "url-loader": "^0.5.7", - "webpack": "^1.12.14", - "webpack-hot-middleware": "^2.10.0" - }, "dependencies": { "@angular/common": "2.0.0-rc.4", "@angular/compiler": "2.0.0-rc.4", @@ -29,12 +12,27 @@ "@angular/router": "3.0.0-beta.2", "angular2-universal": "^0.104.5", "aspnet-prerendering": "^1.0.2", + "aspnet-webpack": "^1.0.6", + "bootstrap": "^3.3.6", "css": "^2.2.1", + "css-loader": "^0.23.1", "es6-shim": "^0.35.1", + "expose-loader": "^0.7.1", + "extendify": "^1.0.0", + "extract-text-webpack-plugin": "^1.0.1", + "file-loader": "^0.8.5", "isomorphic-fetch": "^2.2.1", + "jquery": "^2.2.1", "preboot": "^2.0.10", + "raw-loader": "^0.5.1", "rxjs": "5.0.0-beta.6", + "style-loader": "^0.13.0", + "ts-loader": "^0.8.1", + "typescript": "^1.8.2", + "url-loader": "^0.5.7", + "webpack": "^1.12.14", "webpack-externals-plugin": "^1.0.0", + "webpack-hot-middleware": "^2.10.0", "zone.js": "^0.6.12" } } diff --git a/templates/KnockoutSpa/package.json b/templates/KnockoutSpa/package.json index 96a49bd..bff49e0 100644 --- a/templates/KnockoutSpa/package.json +++ b/templates/KnockoutSpa/package.json @@ -19,9 +19,7 @@ "ts-loader": "^0.8.1", "typescript": "^1.8.2", "url-loader": "^0.5.7", - "webpack": "^1.12.14" - }, - "dependencies": { + "webpack": "^1.12.14", "webpack-hot-middleware": "^2.10.0" } } diff --git a/templates/ReactReduxSpa/package.json b/templates/ReactReduxSpa/package.json index 82fb491..6ac0fee 100644 --- a/templates/ReactReduxSpa/package.json +++ b/templates/ReactReduxSpa/package.json @@ -1,29 +1,21 @@ { "name": "WebApplicationBasic", "version": "0.0.0", - "devDependencies": { + "dependencies": { + "aspnet-prerendering": "^1.0.2", "aspnet-webpack": "^1.0.6", "aspnet-webpack-react": "^1.0.2", + "babel-core": "^6.5.2", "babel-loader": "^6.2.3", "babel-preset-es2015": "^6.5.0", "babel-preset-react": "^6.5.0", "bootstrap": "^3.3.6", "css-loader": "^0.23.1", + "domain-task": "^2.0.0", "extendify": "^1.0.0", "extract-text-webpack-plugin": "^1.0.1", "file-loader": "^0.8.5", "jquery": "^2.2.1", - "style-loader": "^0.13.0", - "ts-loader": "^0.8.1", - "typescript": "^1.8.2", - "url-loader": "^0.5.7", - "webpack": "^1.12.14", - "webpack-hot-middleware": "^2.10.0" - }, - "dependencies": { - "aspnet-prerendering": "^1.0.2", - "babel-core": "^6.5.2", - "domain-task": "^2.0.0", "react": "^15.0.1", "react-dom": "^15.0.1", "react-redux": "^4.4.4", @@ -32,6 +24,12 @@ "redux": "^3.4.0", "redux-thunk": "^2.0.1", "redux-typed": "^1.0.0", - "webpack-externals-plugin": "^1.0.0" + "style-loader": "^0.13.0", + "ts-loader": "^0.8.1", + "typescript": "^1.8.2", + "url-loader": "^0.5.7", + "webpack-externals-plugin": "^1.0.0", + "webpack": "^1.12.14", + "webpack-hot-middleware": "^2.10.0" } } diff --git a/templates/ReactSpa/package.json b/templates/ReactSpa/package.json index 9e07d1b..5d2c44b 100644 --- a/templates/ReactSpa/package.json +++ b/templates/ReactSpa/package.json @@ -4,6 +4,7 @@ "devDependencies": { "aspnet-webpack": "^1.0.6", "aspnet-webpack-react": "^1.0.2", + "babel-core": "^6.5.2", "babel-loader": "^6.2.3", "babel-preset-es2015": "^6.5.0", "babel-preset-react": "^6.5.0", @@ -12,19 +13,16 @@ "extendify": "^1.0.0", "extract-text-webpack-plugin": "^1.0.1", "file-loader": "^0.8.5", + "isomorphic-fetch": "^2.2.1", "jquery": "^2.2.1", + "react": "^15.0.1", + "react-dom": "^15.0.1", + "react-router": "^2.1.1", "style-loader": "^0.13.0", "ts-loader": "^0.8.1", "typescript": "^1.8.2", "url-loader": "^0.5.7", "webpack": "^1.12.14", "webpack-hot-middleware": "^2.10.0" - }, - "dependencies": { - "babel-core": "^6.5.2", - "isomorphic-fetch": "^2.2.1", - "react": "^15.0.1", - "react-dom": "^15.0.1", - "react-router": "^2.1.1" } } From da662c55fadacc4ec75167d9d56c9c8dcd756be0 Mon Sep 17 00:00:00 2001 From: Erik Medina Date: Sat, 27 Aug 2016 03:42:37 -0700 Subject: [PATCH 41/55] Make webpack dev dependency a peer dependency in aspnet-webpack. Moving webpack from a dev dependency to a peer dependency makes the dependency soft and allows the webpack-dev-middleware to pickup the version of webpack being used by the consumer of the package. --- .../npm/aspnet-webpack/package.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-webpack/package.json b/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-webpack/package.json index 327d65d..1f9a584 100644 --- a/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-webpack/package.json +++ b/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-webpack/package.json @@ -14,12 +14,14 @@ "connect": "^3.4.1", "memory-fs": "^0.3.0", "require-from-string": "^1.1.0", - "webpack": "^1.12.14", - "webpack-dev-middleware": "^1.5.1", + "webpack-dev-middleware": "^1.6.1", "webpack-externals-plugin": "^1.0.0" }, "devDependencies": { "rimraf": "^2.5.4", "typescript": "^1.8.10" + }, + "peerDependencies": { + "webpack": "^1.13.2" } } From 67f7e7450f868b25f138f15c209bba00ea09796e Mon Sep 17 00:00:00 2001 From: Erik Medina Date: Sat, 27 Aug 2016 04:03:23 -0700 Subject: [PATCH 42/55] Adding tsd to dev dependencies in aspnet-webpack. Adding tsd to aspnet-webpack's dev dependencies to allow the package's npm prepublish script to succeed without a global tsd install. --- .../npm/aspnet-webpack/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-webpack/package.json b/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-webpack/package.json index 1f9a584..5fa571a 100644 --- a/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-webpack/package.json +++ b/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-webpack/package.json @@ -18,6 +18,7 @@ "webpack-externals-plugin": "^1.0.0" }, "devDependencies": { + "tsd": "0.6.5", "rimraf": "^2.5.4", "typescript": "^1.8.10" }, From 605090e909e53e655ab1ad66be34cc75f2afffed Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Fri, 9 Sep 2016 11:37:57 +0100 Subject: [PATCH 43/55] Publish updated version of aspnet-webpack as 1.0.10 --- .../npm/aspnet-webpack/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-webpack/package.json b/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-webpack/package.json index 5fa571a..e58c2b4 100644 --- a/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-webpack/package.json +++ b/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-webpack/package.json @@ -1,6 +1,6 @@ { "name": "aspnet-webpack", - "version": "1.0.9", + "version": "1.0.10", "description": "Helpers for using Webpack in ASP.NET Core projects. Works in conjunction with the Microsoft.AspNetCore.SpaServices NuGet package.", "main": "index.js", "scripts": { From bc2de2ad598d90c29e19f1b585afbf074384a93c Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Fri, 9 Sep 2016 13:21:31 +0100 Subject: [PATCH 44/55] In aspnet-webpack HMR, don't rely on assumption that entry point is called 'main'. Fixes #289. --- .../npm/aspnet-webpack/package.json | 2 +- .../src/WebpackDevMiddleware.ts | 24 +++++++++++++++---- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-webpack/package.json b/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-webpack/package.json index e58c2b4..077e6ff 100644 --- a/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-webpack/package.json +++ b/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-webpack/package.json @@ -1,6 +1,6 @@ { "name": "aspnet-webpack", - "version": "1.0.10", + "version": "1.0.11", "description": "Helpers for using Webpack in ASP.NET Core projects. Works in conjunction with the Microsoft.AspNetCore.SpaServices NuGet package.", "main": "index.js", "scripts": { diff --git a/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-webpack/src/WebpackDevMiddleware.ts b/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-webpack/src/WebpackDevMiddleware.ts index f34eefd..ff47855 100644 --- a/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-webpack/src/WebpackDevMiddleware.ts +++ b/src/Microsoft.AspNetCore.SpaServices/npm/aspnet-webpack/src/WebpackDevMiddleware.ts @@ -43,12 +43,26 @@ export function createWebpackDevServer(callback: CreateDevServerCallback, option const listener = app.listen(suggestedHMRPortOrZero, () => { // Build the final Webpack config based on supplied options if (enableHotModuleReplacement) { - // TODO: Stop assuming there's an entry point called 'main' - if (typeof webpackConfig.entry['main'] === 'string') { - webpackConfig.entry['main'] = ['webpack-hot-middleware/client', webpackConfig.entry['main']]; - } else { - webpackConfig.entry['main'].unshift('webpack-hot-middleware/client'); + // For this, we only support the key/value config format, not string or string[], since + // those ones don't clearly indicate what the resulting bundle name will be + const entryPoints = webpackConfig.entry; + const isObjectStyleConfig = entryPoints + && typeof entryPoints === 'object' + && !(entryPoints instanceof Array); + if (!isObjectStyleConfig) { + callback('To use HotModuleReplacement, your webpack config must specify an \'entry\' value as a key-value object (e.g., "entry: { main: \'ClientApp/boot-client.ts\' }")', null); + return; } + + // Augment all entry points so they support HMR + Object.getOwnPropertyNames(entryPoints).forEach(entryPointName => { + if (typeof entryPoints[entryPointName] === 'string') { + entryPoints[entryPointName] = ['webpack-hot-middleware/client', entryPoints[entryPointName]]; + } else { + entryPoints[entryPointName].unshift('webpack-hot-middleware/client'); + } + }); + webpackConfig.plugins.push( new webpack.HotModuleReplacementPlugin() ); From f071590fcefa137919d3d500c0180bf385c93248 Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Fri, 9 Sep 2016 16:31:15 +0100 Subject: [PATCH 45/55] Webpack HMR EventSource requests are now proxied (rather than redirected) to the local HMR server. Fixes #271. --- .../Webpack/ConditionalProxyMiddleware.cs | 1 + .../ConditionalProxyMiddlewareOptions.cs | 6 +++- .../Webpack/WebpackDevMiddleware.cs | 36 +++++++++---------- 3 files changed, 22 insertions(+), 21 deletions(-) diff --git a/src/Microsoft.AspNetCore.SpaServices/Webpack/ConditionalProxyMiddleware.cs b/src/Microsoft.AspNetCore.SpaServices/Webpack/ConditionalProxyMiddleware.cs index 72b66c3..5a56c81 100644 --- a/src/Microsoft.AspNetCore.SpaServices/Webpack/ConditionalProxyMiddleware.cs +++ b/src/Microsoft.AspNetCore.SpaServices/Webpack/ConditionalProxyMiddleware.cs @@ -30,6 +30,7 @@ namespace Microsoft.AspNetCore.SpaServices.Webpack _pathPrefix = pathPrefix; _options = options; _httpClient = new HttpClient(new HttpClientHandler()); + _httpClient.Timeout = _options.RequestTimeout; } public async Task Invoke(HttpContext context) diff --git a/src/Microsoft.AspNetCore.SpaServices/Webpack/ConditionalProxyMiddlewareOptions.cs b/src/Microsoft.AspNetCore.SpaServices/Webpack/ConditionalProxyMiddlewareOptions.cs index 5654007..2c3311a 100644 --- a/src/Microsoft.AspNetCore.SpaServices/Webpack/ConditionalProxyMiddlewareOptions.cs +++ b/src/Microsoft.AspNetCore.SpaServices/Webpack/ConditionalProxyMiddlewareOptions.cs @@ -1,16 +1,20 @@ +using System; + namespace Microsoft.AspNetCore.SpaServices.Webpack { internal class ConditionalProxyMiddlewareOptions { - public ConditionalProxyMiddlewareOptions(string scheme, string host, string port) + public ConditionalProxyMiddlewareOptions(string scheme, string host, string port, TimeSpan requestTimeout) { Scheme = scheme; Host = host; Port = port; + RequestTimeout = requestTimeout; } public string Scheme { get; } public string Host { get; } public string Port { get; } + public TimeSpan RequestTimeout { 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 dcc5d4f..d046c28 100644 --- a/src/Microsoft.AspNetCore.SpaServices/Webpack/WebpackDevMiddleware.cs +++ b/src/Microsoft.AspNetCore.SpaServices/Webpack/WebpackDevMiddleware.cs @@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.PlatformAbstractions; using Newtonsoft.Json; +using System.Threading; // Putting in this namespace so it's always available whenever MapRoute is @@ -14,8 +15,6 @@ namespace Microsoft.AspNetCore.Builder { public static class WebpackDevMiddleware { - private const string WebpackDevMiddlewareScheme = "http"; - private const string WebpackHotMiddlewareEndpoint = "/__webpack_hmr"; private const string DefaultConfigFile = "webpack.config.js"; public static void UseWebpackDevMiddleware( @@ -62,30 +61,27 @@ namespace Microsoft.AspNetCore.Builder JsonConvert.SerializeObject(devServerOptions)).Result; // Proxy the corresponding requests through ASP.NET and into the Node listener + // Anything under / (e.g., /dist) is proxied as a normal HTTP request with a typical timeout (100s is the default from HttpClient), + // plus /__webpack_hmr is proxied with infinite timeout, because it's an EventSource (long-lived request). + appBuilder.UseProxyToLocalWebpackDevMiddleware(devServerInfo.PublicPath, devServerInfo.Port, TimeSpan.FromSeconds(100)); + appBuilder.UseProxyToLocalWebpackDevMiddleware("/__webpack_hmr", devServerInfo.Port, Timeout.InfiniteTimeSpan); + } + + private static void UseProxyToLocalWebpackDevMiddleware(this IApplicationBuilder appBuilder, string publicPath, int proxyToPort, TimeSpan requestTimeout) + { // Note that this is hardcoded to make requests to "localhost" regardless of the hostname of the // server as far as the client is concerned. This is because ConditionalProxyMiddlewareOptions is // the one making the internal HTTP requests, and it's going to be to some port on this machine // because aspnet-webpack hosts the dev server there. We can't use the hostname that the client // sees, because that could be anything (e.g., some upstream load balancer) and we might not be // able to make outbound requests to it from here. - var proxyOptions = new ConditionalProxyMiddlewareOptions(WebpackDevMiddlewareScheme, - "localhost", 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. But note that we must use the hostname that the client - // sees, not "localhost", so that it works even when you're not running on localhost (e.g., Docker). - appBuilder.Map(WebpackHotMiddlewareEndpoint, builder => - { - builder.Use(next => ctx => - { - var hostname = ctx.Request.Host.Host; - ctx.Response.Redirect( - $"{WebpackDevMiddlewareScheme}://{hostname}:{devServerInfo.Port.ToString()}{WebpackHotMiddlewareEndpoint}"); - return Task.FromResult(0); - }); - }); + // Also note that the webpack HMR service always uses HTTP, even if your app server uses HTTPS, + // because the HMR service has no need for HTTPS (the client doesn't see it directly - all traffic + // to it is proxied), and the HMR service couldn't use HTTPS anyway (in general it wouldn't have + // the necessary certificate). + var proxyOptions = new ConditionalProxyMiddlewareOptions( + "http", "localhost", proxyToPort.ToString(), requestTimeout); + appBuilder.UseMiddleware(publicPath, proxyOptions); } #pragma warning disable CS0649 From 80f740a9ed33d5fe2589af1ee5d620cb45d0dcd6 Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Tue, 13 Sep 2016 12:51:23 +0100 Subject: [PATCH 46/55] Revert "Webpack HMR EventSource requests are now proxied (rather than redirected) to the local HMR server" because of 'ECANCELED'/'EPIPE broken pipe' issues. Awaiting feedback from Kestrel team. --- .../Webpack/ConditionalProxyMiddleware.cs | 1 - .../ConditionalProxyMiddlewareOptions.cs | 6 +--- .../Webpack/WebpackDevMiddleware.cs | 36 ++++++++++--------- 3 files changed, 21 insertions(+), 22 deletions(-) diff --git a/src/Microsoft.AspNetCore.SpaServices/Webpack/ConditionalProxyMiddleware.cs b/src/Microsoft.AspNetCore.SpaServices/Webpack/ConditionalProxyMiddleware.cs index 5a56c81..72b66c3 100644 --- a/src/Microsoft.AspNetCore.SpaServices/Webpack/ConditionalProxyMiddleware.cs +++ b/src/Microsoft.AspNetCore.SpaServices/Webpack/ConditionalProxyMiddleware.cs @@ -30,7 +30,6 @@ namespace Microsoft.AspNetCore.SpaServices.Webpack _pathPrefix = pathPrefix; _options = options; _httpClient = new HttpClient(new HttpClientHandler()); - _httpClient.Timeout = _options.RequestTimeout; } public async Task Invoke(HttpContext context) diff --git a/src/Microsoft.AspNetCore.SpaServices/Webpack/ConditionalProxyMiddlewareOptions.cs b/src/Microsoft.AspNetCore.SpaServices/Webpack/ConditionalProxyMiddlewareOptions.cs index 2c3311a..5654007 100644 --- a/src/Microsoft.AspNetCore.SpaServices/Webpack/ConditionalProxyMiddlewareOptions.cs +++ b/src/Microsoft.AspNetCore.SpaServices/Webpack/ConditionalProxyMiddlewareOptions.cs @@ -1,20 +1,16 @@ -using System; - namespace Microsoft.AspNetCore.SpaServices.Webpack { internal class ConditionalProxyMiddlewareOptions { - public ConditionalProxyMiddlewareOptions(string scheme, string host, string port, TimeSpan requestTimeout) + public ConditionalProxyMiddlewareOptions(string scheme, string host, string port) { Scheme = scheme; Host = host; Port = port; - RequestTimeout = requestTimeout; } public string Scheme { get; } public string Host { get; } public string Port { get; } - public TimeSpan RequestTimeout { 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 d046c28..dcc5d4f 100644 --- a/src/Microsoft.AspNetCore.SpaServices/Webpack/WebpackDevMiddleware.cs +++ b/src/Microsoft.AspNetCore.SpaServices/Webpack/WebpackDevMiddleware.cs @@ -7,7 +7,6 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.PlatformAbstractions; using Newtonsoft.Json; -using System.Threading; // Putting in this namespace so it's always available whenever MapRoute is @@ -15,6 +14,8 @@ namespace Microsoft.AspNetCore.Builder { public static class WebpackDevMiddleware { + private const string WebpackDevMiddlewareScheme = "http"; + private const string WebpackHotMiddlewareEndpoint = "/__webpack_hmr"; private const string DefaultConfigFile = "webpack.config.js"; public static void UseWebpackDevMiddleware( @@ -61,27 +62,30 @@ namespace Microsoft.AspNetCore.Builder JsonConvert.SerializeObject(devServerOptions)).Result; // Proxy the corresponding requests through ASP.NET and into the Node listener - // Anything under / (e.g., /dist) is proxied as a normal HTTP request with a typical timeout (100s is the default from HttpClient), - // plus /__webpack_hmr is proxied with infinite timeout, because it's an EventSource (long-lived request). - appBuilder.UseProxyToLocalWebpackDevMiddleware(devServerInfo.PublicPath, devServerInfo.Port, TimeSpan.FromSeconds(100)); - appBuilder.UseProxyToLocalWebpackDevMiddleware("/__webpack_hmr", devServerInfo.Port, Timeout.InfiniteTimeSpan); - } - - private static void UseProxyToLocalWebpackDevMiddleware(this IApplicationBuilder appBuilder, string publicPath, int proxyToPort, TimeSpan requestTimeout) - { // Note that this is hardcoded to make requests to "localhost" regardless of the hostname of the // server as far as the client is concerned. This is because ConditionalProxyMiddlewareOptions is // the one making the internal HTTP requests, and it's going to be to some port on this machine // because aspnet-webpack hosts the dev server there. We can't use the hostname that the client // sees, because that could be anything (e.g., some upstream load balancer) and we might not be // able to make outbound requests to it from here. - // Also note that the webpack HMR service always uses HTTP, even if your app server uses HTTPS, - // because the HMR service has no need for HTTPS (the client doesn't see it directly - all traffic - // to it is proxied), and the HMR service couldn't use HTTPS anyway (in general it wouldn't have - // the necessary certificate). - var proxyOptions = new ConditionalProxyMiddlewareOptions( - "http", "localhost", proxyToPort.ToString(), requestTimeout); - appBuilder.UseMiddleware(publicPath, proxyOptions); + var proxyOptions = new ConditionalProxyMiddlewareOptions(WebpackDevMiddlewareScheme, + "localhost", 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. But note that we must use the hostname that the client + // sees, not "localhost", so that it works even when you're not running on localhost (e.g., Docker). + appBuilder.Map(WebpackHotMiddlewareEndpoint, builder => + { + builder.Use(next => ctx => + { + var hostname = ctx.Request.Host.Host; + ctx.Response.Redirect( + $"{WebpackDevMiddlewareScheme}://{hostname}:{devServerInfo.Port.ToString()}{WebpackHotMiddlewareEndpoint}"); + return Task.FromResult(0); + }); + }); } #pragma warning disable CS0649 From 7f841ff8404f886385659a203c75a57baca010b3 Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Tue, 13 Sep 2016 13:44:51 +0100 Subject: [PATCH 47/55] In Yeoman generator, support passing args from command line (e.g., --framework=angular-2) --- templates/package-builder/src/yeoman/app/index.ts | 7 ++++++- templates/package-builder/src/yeoman/package.json | 3 ++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/templates/package-builder/src/yeoman/app/index.ts b/templates/package-builder/src/yeoman/app/index.ts index 04687a0..0d6af2d 100644 --- a/templates/package-builder/src/yeoman/app/index.ts +++ b/templates/package-builder/src/yeoman/app/index.ts @@ -5,6 +5,9 @@ import * as glob from 'glob'; const yosay = require('yosay'); const toPascalCase = require('to-pascal-case'); +type YeomanPrompt = (opt: yeoman.IPromptOptions | yeoman.IPromptOptions[], callback: (answers: any) => void) => void; +const optionOrPrompt: YeomanPrompt = require('yeoman-option-or-prompt'); + const templates = [ { value: 'angular-2', name: 'Angular 2' }, { value: 'knockout', name: 'Knockout' }, @@ -14,16 +17,18 @@ const templates = [ class MyGenerator extends yeoman.Base { private _answers: any; + private _optionOrPrompt: YeomanPrompt; constructor(args: string | string[], options: any) { super(args, options); + this._optionOrPrompt = optionOrPrompt; this.log(yosay('Welcome to the ASP.NET Core Single-Page App generator!')); } prompting() { const done = this.async(); - this.prompt([{ + this._optionOrPrompt([{ type: 'list', name: 'framework', message: 'Framework', diff --git a/templates/package-builder/src/yeoman/package.json b/templates/package-builder/src/yeoman/package.json index 49302f9..2b1305c 100644 --- a/templates/package-builder/src/yeoman/package.json +++ b/templates/package-builder/src/yeoman/package.json @@ -1,6 +1,6 @@ { "name": "generator-aspnetcore-spa", - "version": "0.2.6", + "version": "0.2.7", "description": "Single-Page App templates for ASP.NET Core", "author": "Microsoft", "license": "Apache-2.0", @@ -15,6 +15,7 @@ "node-uuid": "^1.4.7", "to-pascal-case": "^1.0.0", "yeoman-generator": "^0.20.2", + "yeoman-option-or-prompt": "^1.0.2", "yosay": "^1.1.1" } } From b72435c5cc30a4fedd207e2f6092777f4ea84b12 Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Tue, 13 Sep 2016 13:57:23 +0100 Subject: [PATCH 48/55] Yeoman generator support for optional --projectguid=... CLI argument --- templates/package-builder/src/yeoman/app/index.ts | 3 ++- templates/package-builder/src/yeoman/package.json | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/templates/package-builder/src/yeoman/app/index.ts b/templates/package-builder/src/yeoman/app/index.ts index 0d6af2d..c10038b 100644 --- a/templates/package-builder/src/yeoman/app/index.ts +++ b/templates/package-builder/src/yeoman/app/index.ts @@ -28,6 +28,7 @@ class MyGenerator extends yeoman.Base { prompting() { const done = this.async(); + this.option('projectguid'); this._optionOrPrompt([{ type: 'list', name: 'framework', @@ -41,7 +42,7 @@ class MyGenerator extends yeoman.Base { }], answers => { this._answers = answers; this._answers.namePascalCase = toPascalCase(answers.name); - this._answers.projectGuid = uuid.v4(); + this._answers.projectGuid = this.options['projectguid'] || uuid.v4(); done(); }); } diff --git a/templates/package-builder/src/yeoman/package.json b/templates/package-builder/src/yeoman/package.json index 2b1305c..c26fd16 100644 --- a/templates/package-builder/src/yeoman/package.json +++ b/templates/package-builder/src/yeoman/package.json @@ -1,6 +1,6 @@ { "name": "generator-aspnetcore-spa", - "version": "0.2.7", + "version": "0.2.8", "description": "Single-Page App templates for ASP.NET Core", "author": "Microsoft", "license": "Apache-2.0", From 7c316d5c744c5ec1a378ececd89f7eb46a1ba3c7 Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Wed, 14 Sep 2016 11:36:53 +0100 Subject: [PATCH 49/55] Update to ASP.NET Core 1.0.1. Fixes #309 --- src/Microsoft.AspNetCore.AngularServices/project.json | 2 +- src/Microsoft.AspNetCore.ReactServices/project.json | 2 +- src/Microsoft.AspNetCore.SpaServices/project.json | 2 +- templates/Angular2Spa/project.json | 6 +++--- templates/KnockoutSpa/project.json | 6 +++--- templates/ReactReduxSpa/project.json | 6 +++--- templates/ReactSpa/project.json | 6 +++--- templates/WebApplicationBasic/project.json | 6 +++--- 8 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/Microsoft.AspNetCore.AngularServices/project.json b/src/Microsoft.AspNetCore.AngularServices/project.json index 8b64b73..99aa069 100644 --- a/src/Microsoft.AspNetCore.AngularServices/project.json +++ b/src/Microsoft.AspNetCore.AngularServices/project.json @@ -9,7 +9,7 @@ "defaultNamespace": "Microsoft.AspNetCore.AngularServices" }, "dependencies": { - "Microsoft.AspNetCore.Mvc.TagHelpers": "1.0.0", + "Microsoft.AspNetCore.Mvc.TagHelpers": "1.0.1", "Microsoft.AspNetCore.SpaServices": "1.0.0-*" }, "frameworks": { diff --git a/src/Microsoft.AspNetCore.ReactServices/project.json b/src/Microsoft.AspNetCore.ReactServices/project.json index ca1ae6f..1da0998 100644 --- a/src/Microsoft.AspNetCore.ReactServices/project.json +++ b/src/Microsoft.AspNetCore.ReactServices/project.json @@ -9,7 +9,7 @@ "defaultNamespace": "Microsoft.AspNetCore.ReactServices" }, "dependencies": { - "Microsoft.AspNetCore.Mvc.TagHelpers": "1.0.0", + "Microsoft.AspNetCore.Mvc.TagHelpers": "1.0.1", "Microsoft.AspNetCore.SpaServices": "1.0.0-*" }, "frameworks": { diff --git a/src/Microsoft.AspNetCore.SpaServices/project.json b/src/Microsoft.AspNetCore.SpaServices/project.json index 1c55cd2..38ababb 100644 --- a/src/Microsoft.AspNetCore.SpaServices/project.json +++ b/src/Microsoft.AspNetCore.SpaServices/project.json @@ -8,7 +8,7 @@ "Microsoft" ], "dependencies": { - "Microsoft.AspNetCore.Mvc": "1.0.0", + "Microsoft.AspNetCore.Mvc": "1.0.1", "Microsoft.AspNetCore.NodeServices": "1.0.0-*" }, "frameworks": { diff --git a/templates/Angular2Spa/project.json b/templates/Angular2Spa/project.json index 845fe68..c9066fc 100755 --- a/templates/Angular2Spa/project.json +++ b/templates/Angular2Spa/project.json @@ -1,18 +1,18 @@ { "dependencies": { "Microsoft.NETCore.App": { - "version": "1.0.0", + "version": "1.0.1", "type": "platform" }, "Microsoft.AspNetCore.AngularServices": "1.0.0-*", "Microsoft.AspNetCore.Diagnostics": "1.0.0", - "Microsoft.AspNetCore.Mvc": "1.0.0", + "Microsoft.AspNetCore.Mvc": "1.0.1", "Microsoft.AspNetCore.Razor.Tools": { "version": "1.0.0-preview2-final", "type": "build" }, "Microsoft.AspNetCore.Server.IISIntegration": "1.0.0", - "Microsoft.AspNetCore.Server.Kestrel": "1.0.0", + "Microsoft.AspNetCore.Server.Kestrel": "1.0.1", "Microsoft.AspNetCore.StaticFiles": "1.0.0", "Microsoft.Extensions.Configuration.EnvironmentVariables": "1.0.0", "Microsoft.Extensions.Configuration.Json": "1.0.0", diff --git a/templates/KnockoutSpa/project.json b/templates/KnockoutSpa/project.json index d3e2777..f76bf2a 100755 --- a/templates/KnockoutSpa/project.json +++ b/templates/KnockoutSpa/project.json @@ -1,18 +1,18 @@ { "dependencies": { "Microsoft.NETCore.App": { - "version": "1.0.0", + "version": "1.0.1", "type": "platform" }, "Microsoft.AspNetCore.SpaServices": "1.0.0-*", "Microsoft.AspNetCore.Diagnostics": "1.0.0", - "Microsoft.AspNetCore.Mvc": "1.0.0", + "Microsoft.AspNetCore.Mvc": "1.0.1", "Microsoft.AspNetCore.Razor.Tools": { "version": "1.0.0-preview2-final", "type": "build" }, "Microsoft.AspNetCore.Server.IISIntegration": "1.0.0", - "Microsoft.AspNetCore.Server.Kestrel": "1.0.0", + "Microsoft.AspNetCore.Server.Kestrel": "1.0.1", "Microsoft.AspNetCore.StaticFiles": "1.0.0", "Microsoft.Extensions.Configuration.EnvironmentVariables": "1.0.0", "Microsoft.Extensions.Configuration.Json": "1.0.0", diff --git a/templates/ReactReduxSpa/project.json b/templates/ReactReduxSpa/project.json index 0794a38..4b1ebfa 100755 --- a/templates/ReactReduxSpa/project.json +++ b/templates/ReactReduxSpa/project.json @@ -1,18 +1,18 @@ { "dependencies": { "Microsoft.NETCore.App": { - "version": "1.0.0", + "version": "1.0.1", "type": "platform" }, "Microsoft.AspNetCore.ReactServices": "1.0.0-*", "Microsoft.AspNetCore.Diagnostics": "1.0.0", - "Microsoft.AspNetCore.Mvc": "1.0.0", + "Microsoft.AspNetCore.Mvc": "1.0.1", "Microsoft.AspNetCore.Razor.Tools": { "version": "1.0.0-preview2-final", "type": "build" }, "Microsoft.AspNetCore.Server.IISIntegration": "1.0.0", - "Microsoft.AspNetCore.Server.Kestrel": "1.0.0", + "Microsoft.AspNetCore.Server.Kestrel": "1.0.1", "Microsoft.AspNetCore.StaticFiles": "1.0.0", "Microsoft.Extensions.Configuration.EnvironmentVariables": "1.0.0", "Microsoft.Extensions.Configuration.Json": "1.0.0", diff --git a/templates/ReactSpa/project.json b/templates/ReactSpa/project.json index 0794a38..4b1ebfa 100755 --- a/templates/ReactSpa/project.json +++ b/templates/ReactSpa/project.json @@ -1,18 +1,18 @@ { "dependencies": { "Microsoft.NETCore.App": { - "version": "1.0.0", + "version": "1.0.1", "type": "platform" }, "Microsoft.AspNetCore.ReactServices": "1.0.0-*", "Microsoft.AspNetCore.Diagnostics": "1.0.0", - "Microsoft.AspNetCore.Mvc": "1.0.0", + "Microsoft.AspNetCore.Mvc": "1.0.1", "Microsoft.AspNetCore.Razor.Tools": { "version": "1.0.0-preview2-final", "type": "build" }, "Microsoft.AspNetCore.Server.IISIntegration": "1.0.0", - "Microsoft.AspNetCore.Server.Kestrel": "1.0.0", + "Microsoft.AspNetCore.Server.Kestrel": "1.0.1", "Microsoft.AspNetCore.StaticFiles": "1.0.0", "Microsoft.Extensions.Configuration.EnvironmentVariables": "1.0.0", "Microsoft.Extensions.Configuration.Json": "1.0.0", diff --git a/templates/WebApplicationBasic/project.json b/templates/WebApplicationBasic/project.json index 4464a43..106ade4 100755 --- a/templates/WebApplicationBasic/project.json +++ b/templates/WebApplicationBasic/project.json @@ -1,17 +1,17 @@ { "dependencies": { "Microsoft.NETCore.App": { - "version": "1.0.0", + "version": "1.0.1", "type": "platform" }, "Microsoft.AspNetCore.Diagnostics": "1.0.0", - "Microsoft.AspNetCore.Mvc": "1.0.0", + "Microsoft.AspNetCore.Mvc": "1.0.1", "Microsoft.AspNetCore.Razor.Tools": { "version": "1.0.0-preview2-final", "type": "build" }, "Microsoft.AspNetCore.Server.IISIntegration": "1.0.0", - "Microsoft.AspNetCore.Server.Kestrel": "1.0.0", + "Microsoft.AspNetCore.Server.Kestrel": "1.0.1", "Microsoft.AspNetCore.StaticFiles": "1.0.0", "Microsoft.Extensions.Configuration.EnvironmentVariables": "1.0.0", "Microsoft.Extensions.Configuration.Json": "1.0.0", From d76b013a561594ad0f6b258fd35c524f2db594d7 Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Wed, 14 Sep 2016 12:04:15 +0100 Subject: [PATCH 50/55] WebpackDevMiddleware now uses ProjectPath option consistently. Fixes #307 --- .../Webpack/WebpackDevMiddleware.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Microsoft.AspNetCore.SpaServices/Webpack/WebpackDevMiddleware.cs b/src/Microsoft.AspNetCore.SpaServices/Webpack/WebpackDevMiddleware.cs index dcc5d4f..e6a65cb 100644 --- a/src/Microsoft.AspNetCore.SpaServices/Webpack/WebpackDevMiddleware.cs +++ b/src/Microsoft.AspNetCore.SpaServices/Webpack/WebpackDevMiddleware.cs @@ -42,6 +42,11 @@ namespace Microsoft.AspNetCore.Builder // as fast as some theoretical future alternative. var nodeServicesOptions = new NodeServicesOptions(appBuilder.ApplicationServices); nodeServicesOptions.WatchFileExtensions = new string[] {}; // Don't watch anything + if (!string.IsNullOrEmpty(options.ProjectPath)) + { + nodeServicesOptions.ProjectPath = options.ProjectPath; + } + var nodeServices = NodeServicesFactory.CreateNodeServices(nodeServicesOptions); // Get a filename matching the middleware Node script @@ -50,11 +55,9 @@ namespace Microsoft.AspNetCore.Builder var nodeScript = new StringAsTempFile(script); // Will be cleaned up on process exit // Tell Node to start the server hosting webpack-dev-middleware - var hostEnv = (IHostingEnvironment)appBuilder.ApplicationServices.GetService(typeof(IHostingEnvironment)); - var projectPath = options.ProjectPath ?? hostEnv.ContentRootPath; var devServerOptions = new { - webpackConfigPath = Path.Combine(projectPath, options.ConfigFile ?? DefaultConfigFile), + webpackConfigPath = Path.Combine(nodeServicesOptions.ProjectPath, options.ConfigFile ?? DefaultConfigFile), suppliedOptions = options }; var devServerInfo = From 7a80d905b8af277972201ff7a4fb249733a86e2d Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Thu, 15 Sep 2016 12:07:49 +0100 Subject: [PATCH 51/55] In Angular 2 template, include reflect-metadata and zone.js in vendor bundle --- templates/Angular2Spa/webpack.config.vendor.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/templates/Angular2Spa/webpack.config.vendor.js b/templates/Angular2Spa/webpack.config.vendor.js index cd2bbe0..9636722 100644 --- a/templates/Angular2Spa/webpack.config.vendor.js +++ b/templates/Angular2Spa/webpack.config.vendor.js @@ -29,6 +29,8 @@ module.exports = { '@angular/platform-browser-dynamic', '@angular/router', '@angular/platform-server', + 'reflect-metadata', + 'zone.js', ] }, output: { From 06ad36f8309c08aeed9d78cc12076f516855ffa3 Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Thu, 15 Sep 2016 12:32:01 +0100 Subject: [PATCH 52/55] In Angular 2 template, include prebuilt wwwroot/dist/* files to support VS and "dotnet new" templates (which can't run post-project-creation actions) --- templates/package-builder/src/build/build.ts | 34 ++++++++++++++------ 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/templates/package-builder/src/build/build.ts b/templates/package-builder/src/build/build.ts index a0433bd..9c48a76 100644 --- a/templates/package-builder/src/build/build.ts +++ b/templates/package-builder/src/build/build.ts @@ -11,8 +11,8 @@ const isWindows = /^win/.test(process.platform); const textFileExtensions = ['.gitignore', 'template_gitignore', '.config', '.cs', '.cshtml', 'Dockerfile', '.html', '.js', '.json', '.jsx', '.md', '.nuspec', '.ts', '.tsx', '.xproj']; const yeomanGeneratorSource = './src/yeoman'; -const templates: { [key: string]: { dir: string, dotNetNewId: string, displayName: string } } = { - 'angular-2': { dir: '../../templates/Angular2Spa/', dotNetNewId: 'Angular', displayName: 'Angular 2' }, +const templates: { [key: string]: { dir: string, dotNetNewId: string, displayName: string, forceInclusion?: RegExp } } = { + 'angular-2': { dir: '../../templates/Angular2Spa/', dotNetNewId: 'Angular', displayName: 'Angular 2', forceInclusion: /^wwwroot\/dist\// }, 'knockout': { dir: '../../templates/KnockoutSpa/', dotNetNewId: 'Knockout', displayName: 'Knockout.js' }, 'react-redux': { dir: '../../templates/ReactReduxSpa/', dotNetNewId: 'ReactRedux', displayName: 'React.js and Redux' }, 'react': { dir: '../../templates/ReactSpa/', dotNetNewId: 'React', displayName: 'React.js' } @@ -28,7 +28,7 @@ function writeFileEnsuringDirExists(root: string, filename: string, contents: st fs.writeFileSync(fullPath, contents); } -function listFilesExcludingGitignored(root: string): string[] { +function listFilesExcludingGitignored(root: string, forceInclusion: RegExp): string[] { // Note that the gitignore files, prior to be written by the generator, are called 'template_gitignore' // instead of '.gitignore'. This is a workaround for Yeoman doing strange stuff with .gitignore files // (it renames them to .npmignore, which is not helpful). @@ -37,11 +37,11 @@ function listFilesExcludingGitignored(root: string): string[] { ? gitignore.compile(fs.readFileSync(gitIgnorePath, 'utf8')) : { accepts: () => true }; return glob.sync('**/*', { cwd: root, dot: true, nodir: true }) - .filter(fn => gitignoreEvaluator.accepts(fn)); + .filter(fn => gitignoreEvaluator.accepts(fn) || (forceInclusion && forceInclusion.test(fn))); } -function writeTemplate(sourceRoot: string, destRoot: string, contentReplacements: { from: RegExp, to: string }[], filenameReplacements: { from: RegExp, to: string }[]) { - listFilesExcludingGitignored(sourceRoot).forEach(fn => { +function writeTemplate(sourceRoot: string, destRoot: string, contentReplacements: { from: RegExp, to: string }[], filenameReplacements: { from: RegExp, to: string }[], forceInclusion: RegExp) { + listFilesExcludingGitignored(sourceRoot, forceInclusion).forEach(fn => { let sourceContent = fs.readFileSync(path.join(sourceRoot, fn)); // For text files, replace hardcoded values with template tags @@ -89,7 +89,7 @@ function buildYeomanNpmPackage() { ]; _.forEach(templates, (templateConfig, templateName) => { const outputDir = path.join(outputTemplatesRoot, templateName); - writeTemplate(templateConfig.dir, outputDir, contentReplacements, filenameReplacements); + writeTemplate(templateConfig.dir, outputDir, contentReplacements, filenameReplacements, templateConfig.forceInclusion); }); // Also copy the generator files (that's the compiled .js files, plus all other non-.ts files) @@ -125,7 +125,7 @@ function buildDotNetNewNuGetPackage() { _.forEach(templates, (templateConfig, templateName) => { const templateOutputDir = path.join(outputRoot, 'templates', templateName); const templateOutputProjectDir = path.join(templateOutputDir, sourceProjectName); - writeTemplate(templateConfig.dir, templateOutputProjectDir, contentReplacements, filenameReplacements); + writeTemplate(templateConfig.dir, templateOutputProjectDir, contentReplacements, filenameReplacements, templateConfig.forceInclusion); // Add a .netnew.json file fs.writeFileSync(path.join(templateOutputDir, '.netnew.json'), JSON.stringify({ @@ -145,7 +145,7 @@ function buildDotNetNewNuGetPackage() { const yeomanPackageVersion = JSON.parse(fs.readFileSync(path.join(yeomanGeneratorSource, 'package.json'), 'utf8')).version; writeTemplate('./src/dotnetnew', outputRoot, [ { from: /\{version\}/g, to: yeomanPackageVersion }, - ], []); + ], [], null); const nugetExe = path.join(process.cwd(), './bin/NuGet.exe'); const nugetStartInfo = { cwd: outputRoot, stdio: 'inherit' }; if (isWindows) { @@ -160,5 +160,21 @@ function buildDotNetNewNuGetPackage() { rimraf.sync('./tmp'); } +// TODO: Instead of just showing this warning, improve build script so it actually does build them +// in the correct format. Can do this once we've moved away from using ASPNETCORE_ENVIRONMENT to +// control the build output mode. The templates we warn about here are the ones where we ship some +// files that wouldn't normally be under source control (e.g., /wwwroot/dist/*). +const templatesWithForceIncludes = Object.getOwnPropertyNames(templates) + .filter(templateName => !!templates[templateName].forceInclusion); +if (templatesWithForceIncludes.length > 0) { + console.warn(` +--- +WARNING: Ensure that the following templates are already built in the configuration desired for publishing. +For example, build the dist files in debug mode. +TEMPLATES: ${templatesWithForceIncludes.join(', ')} +--- +`); +} + buildYeomanNpmPackage(); buildDotNetNewNuGetPackage(); From 591d548de750e002334e131965f8abbc42aca616 Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Thu, 15 Sep 2016 12:34:24 +0100 Subject: [PATCH 53/55] Publish new Yeoman templates (0.2.9) --- templates/package-builder/src/yeoman/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/package-builder/src/yeoman/package.json b/templates/package-builder/src/yeoman/package.json index c26fd16..164fa30 100644 --- a/templates/package-builder/src/yeoman/package.json +++ b/templates/package-builder/src/yeoman/package.json @@ -1,6 +1,6 @@ { "name": "generator-aspnetcore-spa", - "version": "0.2.8", + "version": "0.2.9", "description": "Single-Page App templates for ASP.NET Core", "author": "Microsoft", "license": "Apache-2.0", From 2ee0078cfd8e2936a51dd6ecfb813d83b03a6ce5 Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Thu, 15 Sep 2016 14:15:03 +0100 Subject: [PATCH 54/55] Fix HttpNodeInstanceEntryPoint to match latest NPM modules --- .../Content/Node/entrypoint-http.js | 5 ++--- .../TypeScript/HttpNodeInstanceEntryPoint.ts | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/Microsoft.AspNetCore.NodeServices/Content/Node/entrypoint-http.js b/src/Microsoft.AspNetCore.NodeServices/Content/Node/entrypoint-http.js index cfa15bc..b085a36 100644 --- a/src/Microsoft.AspNetCore.NodeServices/Content/Node/entrypoint-http.js +++ b/src/Microsoft.AspNetCore.NodeServices/Content/Node/entrypoint-http.js @@ -128,9 +128,8 @@ ExitWhenParentExits_1.exitWhenParentExits(parseInt(parsedArgs.parentPid)); function readRequestBodyAsJson(request, callback) { var requestBodyAsString = ''; - request - .on('data', function (chunk) { requestBodyAsString += chunk; }) - .on('end', function () { callback(JSON.parse(requestBodyAsString)); }); + request.on('data', function (chunk) { requestBodyAsString += chunk; }); + request.on('end', function () { callback(JSON.parse(requestBodyAsString)); }); } function respondWithError(res, errorValue) { res.statusCode = 500; diff --git a/src/Microsoft.AspNetCore.NodeServices/TypeScript/HttpNodeInstanceEntryPoint.ts b/src/Microsoft.AspNetCore.NodeServices/TypeScript/HttpNodeInstanceEntryPoint.ts index f8e619d..eb7fc28 100644 --- a/src/Microsoft.AspNetCore.NodeServices/TypeScript/HttpNodeInstanceEntryPoint.ts +++ b/src/Microsoft.AspNetCore.NodeServices/TypeScript/HttpNodeInstanceEntryPoint.ts @@ -80,9 +80,8 @@ exitWhenParentExits(parseInt(parsedArgs.parentPid)); function readRequestBodyAsJson(request, callback) { let requestBodyAsString = ''; - request - .on('data', chunk => { requestBodyAsString += chunk; }) - .on('end', () => { callback(JSON.parse(requestBodyAsString)); }); + request.on('data', chunk => { requestBodyAsString += chunk; }); + request.on('end', () => { callback(JSON.parse(requestBodyAsString)); }); } function respondWithError(res: http.ServerResponse, errorValue: any) { From b71d139eb5f04d0a04494bcf8cfd7384513d3835 Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Thu, 15 Sep 2016 14:36:42 +0100 Subject: [PATCH 55/55] Update xproj files to reference dotnet build tooling --- templates/Angular2Spa/Angular2Spa.xproj | 8 ++++---- templates/KnockoutSpa/KnockoutSpa.xproj | 12 ++++++------ templates/ReactReduxSpa/ReactReduxSpa.xproj | 8 ++++---- templates/ReactSpa/ReactSpa.xproj | 8 ++++---- .../WebApplicationBasic/WebApplicationBasic.xproj | 8 ++++---- 5 files changed, 22 insertions(+), 22 deletions(-) diff --git a/templates/Angular2Spa/Angular2Spa.xproj b/templates/Angular2Spa/Angular2Spa.xproj index d4c5f07..3f02ad9 100644 --- a/templates/Angular2Spa/Angular2Spa.xproj +++ b/templates/Angular2Spa/Angular2Spa.xproj @@ -5,16 +5,16 @@ $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) true - + 8f5cb8a9-3086-4b49-a1c2-32a9f89bca11 Angular2Spa - ..\..\..\JavaScriptServices.sln\artifacts\obj\$(MSBuildProjectName) + .\obj .\bin\ + v4.5.2 2.0 - 2018 - + \ No newline at end of file diff --git a/templates/KnockoutSpa/KnockoutSpa.xproj b/templates/KnockoutSpa/KnockoutSpa.xproj index 7ce39c5..9a6e512 100644 --- a/templates/KnockoutSpa/KnockoutSpa.xproj +++ b/templates/KnockoutSpa/KnockoutSpa.xproj @@ -5,16 +5,16 @@ $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) true - + 85231b41-6998-49ae-abd2-5124c83dbef2 KnockoutSpa - ..\..\..\JavaScriptServices.sln\artifacts\obj\$(MSBuildProjectName) - ..\..\..\JavaScriptServices.sln\artifacts\bin\$(MSBuildProjectName)\ + .\obj + .\bin\ + v4.5.2 2.0 - 2018 - - + + \ No newline at end of file diff --git a/templates/ReactReduxSpa/ReactReduxSpa.xproj b/templates/ReactReduxSpa/ReactReduxSpa.xproj index 0996bb6..f3701ba 100644 --- a/templates/ReactReduxSpa/ReactReduxSpa.xproj +++ b/templates/ReactReduxSpa/ReactReduxSpa.xproj @@ -5,16 +5,16 @@ $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) true - + dbfc6db0-a6d1-4694-a108-1c604b988da3 ReactReduxSpa - ..\..\..\JavaScriptServices.sln\artifacts\obj\$(MSBuildProjectName) + .\obj .\bin\ + v4.5.2 2.0 - 2018 - + \ No newline at end of file diff --git a/templates/ReactSpa/ReactSpa.xproj b/templates/ReactSpa/ReactSpa.xproj index abe5a58..4a9c25b 100644 --- a/templates/ReactSpa/ReactSpa.xproj +++ b/templates/ReactSpa/ReactSpa.xproj @@ -5,16 +5,16 @@ $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) true - + e9d1a695-f0e6-46f2-b5e3-72f4af805387 ReactSpa - ..\..\..\JavaScriptServices.sln\artifacts\obj\$(MSBuildProjectName) + .\obj .\bin\ + v4.5.2 2.0 - 2018 - + \ No newline at end of file diff --git a/templates/WebApplicationBasic/WebApplicationBasic.xproj b/templates/WebApplicationBasic/WebApplicationBasic.xproj index b0c23a0..40c6b3a 100644 --- a/templates/WebApplicationBasic/WebApplicationBasic.xproj +++ b/templates/WebApplicationBasic/WebApplicationBasic.xproj @@ -5,16 +5,16 @@ $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) true - + cb4398d6-b7f1-449a-ae02-828769679232 WebApplicationBasic - ..\..\..\JavaScriptServices.sln\artifacts\obj\$(MSBuildProjectName) + .\obj .\bin\ + v4.5.2 2.0 - 2018 - + \ No newline at end of file