Merge remote-tracking branch 'refs/remotes/aspnet/dev' into dev

This commit is contained in:
Mark Pieszak
2016-09-16 11:21:00 -04:00
114 changed files with 1565 additions and 681 deletions

1
.gitignore vendored
View File

@@ -36,3 +36,4 @@ npm-debug.log
# repo have to be excluded here. # repo have to be excluded here.
/templates/*/node_modules/ /templates/*/node_modules/
/templates/*/wwwroot/dist/ /templates/*/wwwroot/dist/
.vscode/

View File

@@ -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/)) * 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 * 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 ## Creating new applications
@@ -69,8 +69,10 @@ Also in this repo, [the `samples` directory](https://github.com/aspnet/JavaScrip
**To run the samples:** **To run the samples:**
* Clone this repo * Clone this repo
* Change directory to the same you want to run (e.g., `cd samples/angular/MusicStore`) * At the repo's root directory (the one containing `src`, `samples`, etc.), run `dotnet restore`
* Restore dependencies (run `dotnet restore` and `npm install`) * 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`) * Run the application (`dotnet run`)
* Browse to [http://localhost:5000](http://localhost:5000) * Browse to [http://localhost:5000](http://localhost:5000)

View File

@@ -4,8 +4,8 @@
<cache vary-by="@Context.Request.Path"> <cache vary-by="@Context.Request.Path">
<app asp-prerender-module="wwwroot/ng-app/boot-server">Loading...</app> <app asp-prerender-module="wwwroot/ng-app/boot-server">Loading...</app>
@await Html.PrimeCache(Url.Action("GenreMenuList", "GenresApi")) @await Html.PrimeCacheAsync(Url.Action("GenreMenuList", "GenresApi"))
@await Html.PrimeCache(Url.Action("MostPopular", "AlbumsApi")) @await Html.PrimeCacheAsync(Url.Action("MostPopular", "AlbumsApi"))
</cache> </cache>
@section scripts { @section scripts {

View File

@@ -3,6 +3,7 @@ using System.Diagnostics;
using System.IO; using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.NodeServices; using Microsoft.AspNetCore.NodeServices;
using Microsoft.Extensions.DependencyInjection;
namespace ConsoleApplication namespace ConsoleApplication
{ {
@@ -12,7 +13,17 @@ namespace ConsoleApplication
public class Program public class Program
{ {
public static void Main(string[] args) { public static void Main(string[] args) {
using (var nodeServices = CreateNodeServices(NodeServicesOptions.DefaultNodeHostingModel)) { // Set up the DI system
var services = new ServiceCollection();
services.AddNodeServices(options => {
options.HostingModel = NodeServicesOptions.DefaultNodeHostingModel;
options.ProjectPath = Directory.GetCurrentDirectory();
options.WatchFileExtensions = new string[] {}; // Don't watch anything
});
var serviceProvider = services.BuildServiceProvider();
// Now instantiate an INodeServices and use it
using (var nodeServices = serviceProvider.GetRequiredService<INodeServices>()) {
MeasureLatency(nodeServices).Wait(); MeasureLatency(nodeServices).Wait();
} }
} }
@@ -34,13 +45,5 @@ namespace ConsoleApplication
Console.WriteLine("\nTotal time: {0:F2} milliseconds", 1000 * elapsedSeconds); Console.WriteLine("\nTotal time: {0:F2} milliseconds", 1000 * elapsedSeconds);
Console.WriteLine("\nTime per invocation: {0:F2} milliseconds", 1000 * elapsedSeconds / requestCount); 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
});
}
} }
} }

View File

@@ -8,7 +8,8 @@
"version": "1.0.0", "version": "1.0.0",
"type": "platform" "type": "platform"
}, },
"Microsoft.AspNetCore.NodeServices": "1.0.0-*" "Microsoft.AspNetCore.NodeServices": "1.0.0-*",
"Microsoft.Extensions.DependencyInjection": "1.0.0"
}, },
"frameworks": { "frameworks": {
"netcoreapp1.0": { "netcoreapp1.0": {

View File

@@ -10,11 +10,11 @@
<ProjectGuid>a8905301-8492-42fd-9e83-f715a0fdc3a2</ProjectGuid> <ProjectGuid>a8905301-8492-42fd-9e83-f715a0fdc3a2</ProjectGuid>
<RootNamespace>Webpack</RootNamespace> <RootNamespace>Webpack</RootNamespace>
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">..\..\..\JavaScriptServices.sln\artifacts\obj\$(MSBuildProjectName)</BaseIntermediateOutputPath> <BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">..\..\..\JavaScriptServices.sln\artifacts\obj\$(MSBuildProjectName)</BaseIntermediateOutputPath>
<OutputPath Condition="'$(OutputPath)'=='' ">..\..\..\JavaScriptServices.sln\artifacts\bin\$(MSBuildProjectName)\</OutputPath> <OutputPath Condition="'$(OutputPath)'=='' ">.\bin\</OutputPath>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
<SchemaVersion>2.0</SchemaVersion> <SchemaVersion>2.0</SchemaVersion>
<DevelopmentServerPort>2018</DevelopmentServerPort> <DevelopmentServerPort>2018</DevelopmentServerPort>
</PropertyGroup> </PropertyGroup>
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.targets" Condition="'$(VSToolsPath)' != ''" /> <Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.targets" Condition="'$(VSToolsPath)' != ''" />
</Project> </Project>

View File

@@ -10,7 +10,7 @@ module.exports = merge({
}, },
module: { module: {
loaders: [ loaders: [
{ test: /\.ts(x?)$/, exclude: /node_modules/, loader: 'ts-loader' } { test: /\.ts(x?)$/, exclude: /node_modules/, loader: 'ts-loader?silent' }
], ],
}, },
entry: { entry: {

View File

@@ -10,11 +10,11 @@
<ProjectGuid>c870a92c-9e3f-4bf2-82b8-5758545a8b7c</ProjectGuid> <ProjectGuid>c870a92c-9e3f-4bf2-82b8-5758545a8b7c</ProjectGuid>
<RootNamespace>MusicStore</RootNamespace> <RootNamespace>MusicStore</RootNamespace>
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">..\..\..\JavaScriptServices.sln\artifacts\obj\$(MSBuildProjectName)</BaseIntermediateOutputPath> <BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">..\..\..\JavaScriptServices.sln\artifacts\obj\$(MSBuildProjectName)</BaseIntermediateOutputPath>
<OutputPath Condition="'$(OutputPath)'=='' ">..\..\..\JavaScriptServices.sln\artifacts\bin\$(MSBuildProjectName)\</OutputPath> <OutputPath Condition="'$(OutputPath)'=='' ">.\bin\</OutputPath>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
<SchemaVersion>2.0</SchemaVersion> <SchemaVersion>2.0</SchemaVersion>
<DevelopmentServerPort>2018</DevelopmentServerPort> <DevelopmentServerPort>2018</DevelopmentServerPort>
</PropertyGroup> </PropertyGroup>
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.targets" Condition="'$(VSToolsPath)' != ''" /> <Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.targets" Condition="'$(VSToolsPath)' != ''" />
</Project> </Project>

View File

@@ -5,6 +5,10 @@ import { fakeData } from '../data/fakeData.js';
import { columnMeta } from '../data/columnMeta.jsx'; import { columnMeta } from '../data/columnMeta.jsx';
const resultsPerPage = 10; const resultsPerPage = 10;
// 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 { export class PeopleGrid extends React.Component {
render() { render() {
var pageIndex = this.props.params ? (this.props.params.pageIndex || 1) - 1 : 0; var pageIndex = this.props.params ? (this.props.params.pageIndex || 1) - 1 : 0;

View File

@@ -2,38 +2,38 @@
"name": "ReactExample", "name": "ReactExample",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"babel-core": "^6.4.5", "babel-core": "^6.13.2",
"bootstrap": "^3.3.5", "bootstrap": "^3.3.7",
"domain-task": "^2.0.0", "domain-task": "^2.0.1",
"formsy-react": "^0.17.0", "formsy-react": "^0.18.1",
"formsy-react-components": "^0.6.3", "formsy-react-components": "^0.8.1",
"griddle-react": "^0.3.1", "griddle-react": "^0.6.1",
"history": "^1.12.6", "history": "^3.0.0",
"memory-fs": "^0.3.0", "memory-fs": "^0.3.0",
"react": "^0.14.7", "react": "^15.3.0",
"react-dom": "^0.14.7", "react-dom": "^15.3.0",
"react-router": "^2.0.0-rc5", "react-router": "^2.6.1",
"require-from-string": "^1.1.0", "require-from-string": "^1.2.0",
"underscore": "^1.8.3", "underscore": "^1.8.3",
"webpack-externals-plugin": "^1.0.0" "webpack-externals-plugin": "^1.0.0"
}, },
"devDependencies": { "devDependencies": {
"aspnet-prerendering": "^1.0.0", "aspnet-prerendering": "^1.0.4",
"aspnet-webpack": "^1.0.3", "aspnet-webpack": "^1.0.9",
"aspnet-webpack-react": "^1.0.1", "aspnet-webpack-react": "^1.0.1",
"babel-loader": "^6.2.1", "babel-loader": "^6.2.4",
"babel-plugin-react-transform": "^2.0.0", "babel-plugin-react-transform": "^2.0.2",
"babel-preset-es2015": "^6.3.13", "babel-preset-es2015": "^6.13.2",
"babel-preset-react": "^6.3.13", "babel-preset-react": "^6.11.1",
"css-loader": "^0.21.0", "css-loader": "^0.23.1",
"express": "^4.13.4", "express": "^4.14.0",
"extract-text-webpack-plugin": "^0.8.2", "extract-text-webpack-plugin": "^1.0.1",
"file-loader": "^0.8.4", "file-loader": "^0.9.0",
"react-transform-hmr": "^1.0.1", "react-transform-hmr": "^1.0.4",
"style-loader": "^0.13.0", "style-loader": "^0.13.1",
"url-loader": "^0.5.6", "url-loader": "^0.5.7",
"webpack": "^1.12.2", "webpack": "^1.13.1",
"webpack-dev-middleware": "^1.5.1", "webpack-dev-middleware": "^1.6.1",
"webpack-hot-middleware": "^2.6.4" "webpack-hot-middleware": "^2.12.2"
} }
} }

View File

@@ -11,9 +11,15 @@ namespace Microsoft.AspNetCore.AngularServices
{ {
public static class PrimeCacheHelper public static class PrimeCacheHelper
{ {
public static async Task<HtmlString> PrimeCache(this IHtmlHelper html, string url) [Obsolete("Use PrimeCacheAsync instead")]
public static Task<IHtmlContent> 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<IHtmlContent> 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 // 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. // HTTP requests made during server-side rendering, without risking unnecessary duplicate requests.

View File

@@ -5,7 +5,7 @@
"main": "./dist/Exports", "main": "./dist/Exports",
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1", "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", "typings": "dist/Exports",
"author": "Microsoft", "author": "Microsoft",
@@ -17,6 +17,7 @@
"devDependencies": { "devDependencies": {
"es6-shim": "^0.35.0", "es6-shim": "^0.35.0",
"reflect-metadata": "^0.1.2", "reflect-metadata": "^0.1.2",
"rimraf": "^2.5.4",
"systemjs-builder": "^0.14.11", "systemjs-builder": "^0.14.11",
"typescript": "^1.8.10", "typescript": "^1.8.10",
"zone.js": "^0.6.10" "zone.js": "^0.6.10"

View File

@@ -9,7 +9,7 @@
"defaultNamespace": "Microsoft.AspNetCore.AngularServices" "defaultNamespace": "Microsoft.AspNetCore.AngularServices"
}, },
"dependencies": { "dependencies": {
"Microsoft.AspNetCore.Mvc.TagHelpers": "1.0.0", "Microsoft.AspNetCore.Mvc.TagHelpers": "1.0.1",
"Microsoft.AspNetCore.SpaServices": "1.0.0-*" "Microsoft.AspNetCore.SpaServices": "1.0.0-*"
}, },
"frameworks": { "frameworks": {

View File

@@ -1,83 +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;
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 =>
{
// Since this instance is being created through DI, we can access the IHostingEnvironment
// to populate options.ProjectPath if it wasn't explicitly specified.
if (string.IsNullOrEmpty(options.ProjectPath))
{
var hostEnv = serviceProvider.GetRequiredService<IHostingEnvironment>();
options.ProjectPath = hostEnv.ContentRootPath;
}
// 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<ILoggerFactory>();
if (loggerFactory != null)
{
options.NodeInstanceOutputLogger = loggerFactory.CreateLogger(LogCategoryName);
}
}
return new NodeServicesImpl(options, () => CreateNodeInstance(options));
});
}
public static INodeServices CreateNodeServices(NodeServicesOptions options)
{
return new NodeServicesImpl(options, () => CreateNodeInstance(options));
}
private static INodeInstance CreateNodeInstance(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
// 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.
switch (options.HostingModel)
{
case NodeHostingModel.Http:
return new HttpNodeInstance(options.ProjectPath, options.WatchFileExtensions, logger,
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);
default:
throw new ArgumentException("Unknown hosting model: " + options.HostingModel);
}
}
}
}
}

View File

@@ -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.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.InvocationTimeoutMilliseconds, options.LaunchWithDebugging, options.DebuggingPort);
default:
throw new ArgumentException("Unknown hosting model: " + options.HostingModel);
}
}
}
}
}

View File

@@ -1,27 +1,57 @@
using System; using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.NodeServices.HostingModels; using Microsoft.AspNetCore.NodeServices.HostingModels;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Logging.Console;
namespace Microsoft.AspNetCore.NodeServices namespace Microsoft.AspNetCore.NodeServices
{ {
public class NodeServicesOptions public class NodeServicesOptions
{ {
public const NodeHostingModel DefaultNodeHostingModel = NodeHostingModel.Http; 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" }; 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<string, string>();
InvocationTimeoutMilliseconds = DefaultInvocationTimeoutMilliseconds;
HostingModel = DefaultNodeHostingModel; HostingModel = DefaultNodeHostingModel;
WatchFileExtensions = (string[])DefaultWatchFileExtensions.Clone(); 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<IHostingEnvironment>();
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<ILoggerFactory>();
NodeInstanceOutputLogger = loggerFactory != null
? loggerFactory.CreateLogger(LogCategoryName)
: new ConsoleLogger(LogCategoryName, null, false);
} }
public Action<System.Diagnostics.ProcessStartInfo> OnBeforeStartExternalProcess { get; set; }
public NodeHostingModel HostingModel { get; set; } public NodeHostingModel HostingModel { get; set; }
public Func<INodeInstance> NodeInstanceFactory { get; set; } public Func<INodeInstance> NodeInstanceFactory { get; set; }
public string ProjectPath { get; set; } public string ProjectPath { get; set; }
public string[] WatchFileExtensions { get; set; } public string[] WatchFileExtensions { get; set; }
public ILogger NodeInstanceOutputLogger { get; set; } public ILogger NodeInstanceOutputLogger { get; set; }
public bool LaunchWithDebugging { get; set; } public bool LaunchWithDebugging { get; set; }
public int? DebuggingPort { get; set; } public IDictionary<string, string> EnvironmentVariables { get; set; }
public int DebuggingPort { get; set; }
public int InvocationTimeoutMilliseconds { get; set; }
} }
} }

View File

@@ -0,0 +1,41 @@
using System;
using Microsoft.AspNetCore.NodeServices;
namespace Microsoft.Extensions.DependencyInjection
{
/// <summary>
/// Extension methods for setting up NodeServices in an <see cref="IServiceCollection" />.
/// </summary>
public static class NodeServicesServiceCollectionExtensions
{
public static void AddNodeServices(this IServiceCollection serviceCollection)
=> AddNodeServices(serviceCollection, _ => {});
[Obsolete("Use the AddNodeServices(Action<NodeServicesOptions> 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<NodeServicesOptions> 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);
});
}
}
}

View File

@@ -58,6 +58,7 @@
var http = __webpack_require__(3); var http = __webpack_require__(3);
var path = __webpack_require__(4); var path = __webpack_require__(4);
var ArgsUtil_1 = __webpack_require__(5); 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 // Webpack doesn't support dynamic requires for files not present at compile time, so grab a direct
// reference to Node's runtime 'require' function. // reference to Node's runtime 'require' function.
var dynamicRequire = eval('require'); var dynamicRequire = eval('require');
@@ -68,18 +69,21 @@
if (!hasSentResult) { if (!hasSentResult) {
hasSentResult = true; hasSentResult = true;
if (errorValue) { if (errorValue) {
res.statusCode = 500; respondWithError(res, errorValue);
if (errorValue.stack) {
res.end(errorValue.stack);
}
else {
res.end(errorValue.toString());
}
} }
else if (typeof successValue !== 'string') { else if (typeof successValue !== 'string') {
// Arbitrary object/number/etc - JSON-serialize it // 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.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify(successValue)); res.end(successValueJson);
} }
else { else {
// String - can bypass JSON-serialization altogether // String - can bypass JSON-serialization altogether
@@ -121,11 +125,15 @@
// Signal to the NodeServices base class that we're ready to accept invocations // Signal to the NodeServices base class that we're ready to accept invocations
console.log('[Microsoft.AspNetCore.NodeServices:Listening]'); console.log('[Microsoft.AspNetCore.NodeServices:Listening]');
}); });
ExitWhenParentExits_1.exitWhenParentExits(parseInt(parsedArgs.parentPid));
function readRequestBodyAsJson(request, callback) { function readRequestBodyAsJson(request, callback) {
var requestBodyAsString = ''; var requestBodyAsString = '';
request request.on('data', function (chunk) { requestBodyAsString += chunk; });
.on('data', function (chunk) { requestBodyAsString += chunk; }) request.on('end', function () { callback(JSON.parse(requestBodyAsString)); });
.on('end', function () { callback(JSON.parse(requestBodyAsString)); }); }
function respondWithError(res, errorValue) {
res.statusCode = 500;
res.end(errorValue.stack || errorValue.toString());
} }
@@ -208,5 +216,72 @@
exports.parseArgs = parseArgs; 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;
}
}
/***/ } /***/ }
/******/ ]))); /******/ ])));

View File

@@ -44,7 +44,7 @@
/* 0 */ /* 0 */
/***/ function(module, exports, __webpack_require__) { /***/ function(module, exports, __webpack_require__) {
module.exports = __webpack_require__(6); module.exports = __webpack_require__(7);
/***/ }, /***/ },
@@ -124,17 +124,85 @@
/***/ }, /***/ },
/* 6 */ /* 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__) { /***/ function(module, exports, __webpack_require__) {
"use strict"; "use strict";
// Limit dependencies to core Node modules. This means the code in this file has to be very low-level and unattractive, // 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. // but simplifies things for the consumer of this module.
__webpack_require__(2); __webpack_require__(2);
var net = __webpack_require__(7); var net = __webpack_require__(8);
var path = __webpack_require__(4); var path = __webpack_require__(4);
var readline = __webpack_require__(8); var readline = __webpack_require__(9);
var ArgsUtil_1 = __webpack_require__(5); 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 // Webpack doesn't support dynamic requires for files not present at compile time, so grab a direct
// reference to Node's runtime 'require' function. // reference to Node's runtime 'require' function.
var dynamicRequire = eval('require'); var dynamicRequire = eval('require');
@@ -189,27 +257,28 @@
var parsedArgs = ArgsUtil_1.parseArgs(process.argv); var parsedArgs = ArgsUtil_1.parseArgs(process.argv);
var listenAddress = (useWindowsNamedPipes ? '\\\\.\\pipe\\' : '/tmp/') + parsedArgs.listenAddress; var listenAddress = (useWindowsNamedPipes ? '\\\\.\\pipe\\' : '/tmp/') + parsedArgs.listenAddress;
server.listen(listenAddress); server.listen(listenAddress);
ExitWhenParentExits_1.exitWhenParentExits(parseInt(parsedArgs.parentPid));
/***/ },
/* 7 */
/***/ function(module, exports) {
module.exports = require("net");
/***/ }, /***/ },
/* 8 */ /* 8 */
/***/ function(module, exports) { /***/ function(module, exports) {
module.exports = require("readline"); module.exports = require("net");
/***/ }, /***/ },
/* 9 */ /* 9 */
/***/ function(module, exports) {
module.exports = require("readline");
/***/ },
/* 10 */
/***/ function(module, exports, __webpack_require__) { /***/ function(module, exports, __webpack_require__) {
"use strict"; "use strict";
var events_1 = __webpack_require__(10); var events_1 = __webpack_require__(11);
var VirtualConnection_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, // 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. // and both will reject longer frames.
var MaxFrameBodyLength = 16 * 1024; var MaxFrameBodyLength = 16 * 1024;
@@ -390,13 +459,13 @@
/***/ }, /***/ },
/* 10 */ /* 11 */
/***/ function(module, exports) { /***/ function(module, exports) {
module.exports = require("events"); module.exports = require("events");
/***/ }, /***/ },
/* 11 */ /* 12 */
/***/ function(module, exports, __webpack_require__) { /***/ function(module, exports, __webpack_require__) {
"use strict"; "use strict";
@@ -405,7 +474,7 @@
function __() { this.constructor = d; } function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); 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. * 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) { /***/ function(module, exports) {
module.exports = require("stream"); module.exports = require("stream");

View File

@@ -1,8 +1,10 @@
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Net.Http; using System.Net.Http;
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Newtonsoft.Json; using Newtonsoft.Json;
@@ -24,9 +26,10 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels
private static readonly Regex PortMessageRegex = private static readonly Regex PortMessageRegex =
new Regex(@"^\[Microsoft.AspNetCore.NodeServices.HttpNodeHost:Listening on port (\d+)\]$"); 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; private readonly HttpClient _client;
@@ -34,7 +37,8 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels
private int _portNumber; private int _portNumber;
public HttpNodeInstance(string projectPath, string[] watchFileExtensions, ILogger nodeInstanceOutputLogger, public HttpNodeInstance(string projectPath, string[] watchFileExtensions, ILogger nodeInstanceOutputLogger,
bool launchWithDebugging, int? debuggingPort, int port = 0) IDictionary<string, string> environmentVars, int invocationTimeoutMilliseconds, bool launchWithDebugging,
int debuggingPort, int port = 0)
: base( : base(
EmbeddedResourceReader.Read( EmbeddedResourceReader.Read(
typeof(HttpNodeInstance), typeof(HttpNodeInstance),
@@ -43,6 +47,8 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels
watchFileExtensions, watchFileExtensions,
MakeCommandLineOptions(port), MakeCommandLineOptions(port),
nodeInstanceOutputLogger, nodeInstanceOutputLogger,
environmentVars,
invocationTimeoutMilliseconds,
launchWithDebugging, launchWithDebugging,
debuggingPort) debuggingPort)
{ {
@@ -54,15 +60,17 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels
return $"--port {port}"; return $"--port {port}";
} }
protected override async Task<T> InvokeExportAsync<T>(NodeInvocationInfo invocationInfo) protected override async Task<T> InvokeExportAsync<T>(
NodeInvocationInfo invocationInfo, CancellationToken cancellationToken)
{ {
var payloadJson = JsonConvert.SerializeObject(invocationInfo, JsonSerializerSettings); var payloadJson = JsonConvert.SerializeObject(invocationInfo, jsonSerializerSettings);
var payload = new StringContent(payloadJson, Encoding.UTF8, "application/json"); 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) 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); throw new Exception("Call to Node module failed with error: " + responseErrorString);
} }
@@ -78,12 +86,12 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels
typeof(T).FullName); typeof(T).FullName);
} }
var responseString = await response.Content.ReadAsStringAsync(); var responseString = await response.Content.ReadAsStringAsync().OrThrowOnCancellation(cancellationToken);
return (T)(object)responseString; return (T)(object)responseString;
case "application/json": case "application/json":
var responseJson = await response.Content.ReadAsStringAsync(); var responseJson = await response.Content.ReadAsStringAsync().OrThrowOnCancellation(cancellationToken);
return JsonConvert.DeserializeObject<T>(responseJson); return JsonConvert.DeserializeObject<T>(responseJson, jsonSerializerSettings);
case "application/octet-stream": case "application/octet-stream":
// Streamed responses have to be received as System.IO.Stream instances // Streamed responses have to be received as System.IO.Stream instances
@@ -94,7 +102,7 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels
typeof(T).FullName + ". Instead you must use the generic type System.IO.Stream."); 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: default:
throw new InvalidOperationException("Unexpected response content type: " + responseContentType.MediaType); throw new InvalidOperationException("Unexpected response content type: " + responseContentType.MediaType);

View File

@@ -1,10 +1,11 @@
using System; using System;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Microsoft.AspNetCore.NodeServices.HostingModels namespace Microsoft.AspNetCore.NodeServices.HostingModels
{ {
public interface INodeInstance : IDisposable public interface INodeInstance : IDisposable
{ {
Task<T> InvokeExportAsync<T>(string moduleName, string exportNameOrNull, params object[] args); Task<T> InvokeExportAsync<T>(CancellationToken cancellationToken, string moduleName, string exportNameOrNull, params object[] args);
} }
} }

View File

@@ -1,7 +1,9 @@
using System; using System;
using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@@ -35,6 +37,7 @@ If you haven't yet installed node-inspector, you can do so as follows:
private bool _disposed; private bool _disposed;
private readonly StringAsTempFile _entryPointScript; private readonly StringAsTempFile _entryPointScript;
private FileSystemWatcher _fileSystemWatcher; private FileSystemWatcher _fileSystemWatcher;
private int _invocationTimeoutMilliseconds;
private readonly Process _nodeProcess; private readonly Process _nodeProcess;
private int? _nodeDebuggingPort; private int? _nodeDebuggingPort;
private bool _nodeProcessNeedsRestart; private bool _nodeProcessNeedsRestart;
@@ -46,8 +49,10 @@ If you haven't yet installed node-inspector, you can do so as follows:
string[] watchFileExtensions, string[] watchFileExtensions,
string commandLineArguments, string commandLineArguments,
ILogger nodeOutputLogger, ILogger nodeOutputLogger,
IDictionary<string, string> environmentVars,
int invocationTimeoutMilliseconds,
bool launchWithDebugging, bool launchWithDebugging,
int? debuggingPort) int debuggingPort)
{ {
if (nodeOutputLogger == null) if (nodeOutputLogger == null)
{ {
@@ -56,16 +61,18 @@ If you haven't yet installed node-inspector, you can do so as follows:
OutputLogger = nodeOutputLogger; OutputLogger = nodeOutputLogger;
_entryPointScript = new StringAsTempFile(entryPointScript); _entryPointScript = new StringAsTempFile(entryPointScript);
_invocationTimeoutMilliseconds = invocationTimeoutMilliseconds;
var startInfo = PrepareNodeProcessStartInfo(_entryPointScript.FileName, projectPath, commandLineArguments, var startInfo = PrepareNodeProcessStartInfo(_entryPointScript.FileName, projectPath, commandLineArguments,
launchWithDebugging, debuggingPort); environmentVars, launchWithDebugging, debuggingPort);
_nodeProcess = LaunchNodeProcess(startInfo); _nodeProcess = LaunchNodeProcess(startInfo);
_watchFileExtensions = watchFileExtensions; _watchFileExtensions = watchFileExtensions;
_fileSystemWatcher = BeginFileWatcher(projectPath); _fileSystemWatcher = BeginFileWatcher(projectPath);
ConnectToInputOutputStreams(); ConnectToInputOutputStreams();
} }
public async Task<T> InvokeExportAsync<T>(string moduleName, string exportNameOrNull, params object[] args) public async Task<T> InvokeExportAsync<T>(
CancellationToken cancellationToken, string moduleName, string exportNameOrNull, params object[] args)
{ {
if (_nodeProcess.HasExited || _nodeProcessNeedsRestart) if (_nodeProcess.HasExited || _nodeProcessNeedsRestart)
{ {
@@ -77,15 +84,74 @@ If you haven't yet installed node-inspector, you can do so as follows:
throw new NodeInvocationException(message, null, nodeInstanceUnavailable: true); throw new NodeInvocationException(message, null, nodeInstanceUnavailable: true);
} }
// Wait until the connection is established. This will throw if the connection fails to initialize. // Construct a new cancellation token that combines the supplied token with the configured invocation
await _connectionIsReadySource.Task; // 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.
return await InvokeExportAsync<T>(new NodeInvocationInfo using (var timeoutSource = new CancellationTokenSource())
using (var combinedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutSource.Token))
{ {
ModuleName = moduleName, if (_invocationTimeoutMilliseconds > 0)
ExportedFunctionName = exportNameOrNull, {
Args = args 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<T>(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() public void Dispose()
@@ -94,17 +160,19 @@ If you haven't yet installed node-inspector, you can do so as follows:
GC.SuppressFinalize(this); GC.SuppressFinalize(this);
} }
protected abstract Task<T> InvokeExportAsync<T>(NodeInvocationInfo invocationInfo); protected abstract Task<T> InvokeExportAsync<T>(
NodeInvocationInfo invocationInfo,
CancellationToken cancellationToken);
// This method is virtual, as it provides a way to override the NODE_PATH or the path to node.exe // This method is virtual, as it provides a way to override the NODE_PATH or the path to node.exe
protected virtual ProcessStartInfo PrepareNodeProcessStartInfo( protected virtual ProcessStartInfo PrepareNodeProcessStartInfo(
string entryPointFilename, string projectPath, string commandLineArguments, string entryPointFilename, string projectPath, string commandLineArguments,
bool launchWithDebugging, int? debuggingPort) IDictionary<string, string> environmentVars, bool launchWithDebugging, int debuggingPort)
{ {
string debuggingArgs; string debuggingArgs;
if (launchWithDebugging) if (launchWithDebugging)
{ {
debuggingArgs = debuggingPort.HasValue ? $"--debug={debuggingPort.Value} " : "--debug "; debuggingArgs = debuggingPort != default(int) ? $"--debug={debuggingPort} " : "--debug ";
_nodeDebuggingPort = debuggingPort; _nodeDebuggingPort = debuggingPort;
} }
else else
@@ -112,9 +180,10 @@ If you haven't yet installed node-inspector, you can do so as follows:
debuggingArgs = string.Empty; debuggingArgs = string.Empty;
} }
var thisProcessPid = Process.GetCurrentProcess().Id;
var startInfo = new ProcessStartInfo("node") var startInfo = new ProcessStartInfo("node")
{ {
Arguments = debuggingArgs + "\"" + entryPointFilename + "\" " + (commandLineArguments ?? string.Empty), Arguments = $"{debuggingArgs}\"{entryPointFilename}\" --parentPid {thisProcessPid} {commandLineArguments ?? string.Empty}",
UseShellExecute = false, UseShellExecute = false,
RedirectStandardInput = true, RedirectStandardInput = true,
RedirectStandardOutput = true, RedirectStandardOutput = true,
@@ -122,6 +191,19 @@ If you haven't yet installed node-inspector, you can do so as follows:
WorkingDirectory = projectPath 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 // Append projectPath to NODE_PATH so it can locate node_modules
var existingNodePath = Environment.GetEnvironmentVariable("NODE_PATH") ?? string.Empty; var existingNodePath = Environment.GetEnvironmentVariable("NODE_PATH") ?? string.Empty;
if (existingNodePath != string.Empty) if (existingNodePath != string.Empty)
@@ -130,11 +212,7 @@ If you haven't yet installed node-inspector, you can do so as follows:
} }
var nodePathValue = existingNodePath + Path.Combine(projectPath, "node_modules"); var nodePathValue = existingNodePath + Path.Combine(projectPath, "node_modules");
#if NET451 SetEnvironmentVariable(startInfo, "NODE_PATH", nodePathValue);
startInfo.EnvironmentVariables["NODE_PATH"] = nodePathValue;
#else
startInfo.Environment["NODE_PATH"] = nodePathValue;
#endif
return startInfo; return startInfo;
} }
@@ -179,6 +257,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) private static Process LaunchNodeProcess(ProcessStartInfo startInfo)
{ {
var process = Process.Start(startInfo); var process = Process.Start(startInfo);

View File

@@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
@@ -28,7 +29,8 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels
{ {
private readonly static JsonSerializerSettings jsonSerializerSettings = new JsonSerializerSettings private readonly static JsonSerializerSettings jsonSerializerSettings = new JsonSerializerSettings
{ {
ContractResolver = new CamelCasePropertyNamesContractResolver() ContractResolver = new CamelCasePropertyNamesContractResolver(),
TypeNameHandling = TypeNameHandling.None
}; };
private readonly SemaphoreSlim _connectionCreationSemaphore = new SemaphoreSlim(1); private readonly SemaphoreSlim _connectionCreationSemaphore = new SemaphoreSlim(1);
@@ -38,7 +40,8 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels
private VirtualConnectionClient _virtualConnectionClient; private VirtualConnectionClient _virtualConnectionClient;
public SocketNodeInstance(string projectPath, string[] watchFileExtensions, string socketAddress, public SocketNodeInstance(string projectPath, string[] watchFileExtensions, string socketAddress,
ILogger nodeInstanceOutputLogger, bool launchWithDebugging, int? debuggingPort) ILogger nodeInstanceOutputLogger, IDictionary<string, string> environmentVars,
int invocationTimeoutMilliseconds, bool launchWithDebugging, int debuggingPort)
: base( : base(
EmbeddedResourceReader.Read( EmbeddedResourceReader.Read(
typeof(SocketNodeInstance), typeof(SocketNodeInstance),
@@ -47,13 +50,15 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels
watchFileExtensions, watchFileExtensions,
MakeNewCommandLineOptions(socketAddress), MakeNewCommandLineOptions(socketAddress),
nodeInstanceOutputLogger, nodeInstanceOutputLogger,
environmentVars,
invocationTimeoutMilliseconds,
launchWithDebugging, launchWithDebugging,
debuggingPort) debuggingPort)
{ {
_socketAddress = socketAddress; _socketAddress = socketAddress;
} }
protected override async Task<T> InvokeExportAsync<T>(NodeInvocationInfo invocationInfo) protected override async Task<T> InvokeExportAsync<T>(NodeInvocationInfo invocationInfo, CancellationToken cancellationToken)
{ {
if (_connectionHasFailed) if (_connectionHasFailed)
{ {
@@ -66,7 +71,12 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels
if (_virtualConnectionClient == null) 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 // For each invocation, we open a new virtual connection. This gives an API equivalent to opening a new
@@ -79,7 +89,7 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels
virtualConnection = _virtualConnectionClient.OpenVirtualConnection(); virtualConnection = _virtualConnectionClient.OpenVirtualConnection();
// Send request // Send request
await WriteJsonLineAsync(virtualConnection, invocationInfo); await WriteJsonLineAsync(virtualConnection, invocationInfo, cancellationToken);
// Determine what kind of response format is expected // Determine what kind of response format is expected
if (typeof(T) == typeof(Stream)) if (typeof(T) == typeof(Stream))
@@ -92,7 +102,7 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels
else else
{ {
// Parse and return non-streamed JSON response // Parse and return non-streamed JSON response
var response = await ReadJsonAsync<RpcJsonResponse<T>>(virtualConnection); var response = await ReadJsonAsync<RpcJsonResponse<T>>(virtualConnection, cancellationToken);
if (response.ErrorMessage != null) if (response.ErrorMessage != null)
{ {
throw new NodeInvocationException(response.ErrorMessage, response.ErrorDetails); throw new NodeInvocationException(response.ErrorMessage, response.ErrorDetails);
@@ -159,27 +169,27 @@ namespace Microsoft.AspNetCore.NodeServices.HostingModels
base.Dispose(disposing); 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 json = JsonConvert.SerializeObject(serializableObject, jsonSerializerSettings);
var bytes = Encoding.UTF8.GetBytes(json + '\n'); 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<T> ReadJsonAsync<T>(Stream stream) private static async Task<T> ReadJsonAsync<T>(Stream stream, CancellationToken cancellationToken)
{ {
var json = Encoding.UTF8.GetString(await ReadAllBytesAsync(stream)); var json = Encoding.UTF8.GetString(await ReadAllBytesAsync(stream, cancellationToken));
return JsonConvert.DeserializeObject<T>(json, jsonSerializerSettings); return JsonConvert.DeserializeObject<T>(json, jsonSerializerSettings);
} }
private static async Task<byte[]> ReadAllBytesAsync(Stream input) private static async Task<byte[]> ReadAllBytesAsync(Stream input, CancellationToken cancellationToken)
{ {
byte[] buffer = new byte[16 * 1024]; byte[] buffer = new byte[16 * 1024];
using (var ms = new MemoryStream()) using (var ms = new MemoryStream())
{ {
int read; 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); ms.Write(buffer, 0, read);
} }

View File

@@ -1,4 +1,5 @@
using System; using System;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Microsoft.AspNetCore.NodeServices namespace Microsoft.AspNetCore.NodeServices
@@ -6,8 +7,11 @@ namespace Microsoft.AspNetCore.NodeServices
public interface INodeServices : IDisposable public interface INodeServices : IDisposable
{ {
Task<T> InvokeAsync<T>(string moduleName, params object[] args); Task<T> InvokeAsync<T>(string moduleName, params object[] args);
Task<T> InvokeAsync<T>(CancellationToken cancellationToken, string moduleName, params object[] args);
Task<T> InvokeExportAsync<T>(string moduleName, string exportedFunctionName, params object[] args); Task<T> InvokeExportAsync<T>(string moduleName, string exportedFunctionName, params object[] args);
Task<T> InvokeExportAsync<T>(CancellationToken cancellationToken, string moduleName, string exportedFunctionName, params object[] args);
[Obsolete("Use InvokeAsync instead")] [Obsolete("Use InvokeAsync instead")]
Task<T> Invoke<T>(string moduleName, params object[] args); Task<T> Invoke<T>(string moduleName, params object[] args);

View File

@@ -1,4 +1,5 @@
using System; using System;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.NodeServices.HostingModels; using Microsoft.AspNetCore.NodeServices.HostingModels;
@@ -19,15 +20,13 @@ namespace Microsoft.AspNetCore.NodeServices
internal class NodeServicesImpl : INodeServices internal class NodeServicesImpl : INodeServices
{ {
private static TimeSpan ConnectionDrainingTimespan = TimeSpan.FromSeconds(15); private static TimeSpan ConnectionDrainingTimespan = TimeSpan.FromSeconds(15);
private NodeServicesOptions _options;
private Func<INodeInstance> _nodeInstanceFactory; private Func<INodeInstance> _nodeInstanceFactory;
private INodeInstance _currentNodeInstance; private INodeInstance _currentNodeInstance;
private object _currentNodeInstanceAccessLock = new object(); private object _currentNodeInstanceAccessLock = new object();
private Exception _instanceDelayedDisposalException; private Exception _instanceDelayedDisposalException;
internal NodeServicesImpl(NodeServicesOptions options, Func<INodeInstance> nodeInstanceFactory) internal NodeServicesImpl(Func<INodeInstance> nodeInstanceFactory)
{ {
_options = options;
_nodeInstanceFactory = nodeInstanceFactory; _nodeInstanceFactory = nodeInstanceFactory;
} }
@@ -36,19 +35,29 @@ namespace Microsoft.AspNetCore.NodeServices
return InvokeExportAsync<T>(moduleName, null, args); return InvokeExportAsync<T>(moduleName, null, args);
} }
public Task<T> InvokeExportAsync<T>(string moduleName, string exportedFunctionName, params object[] args) public Task<T> InvokeAsync<T>(CancellationToken cancellationToken, string moduleName, params object[] args)
{ {
return InvokeExportWithPossibleRetryAsync<T>(moduleName, exportedFunctionName, args, allowRetry: true); return InvokeExportAsync<T>(cancellationToken, moduleName, null, args);
} }
public async Task<T> InvokeExportWithPossibleRetryAsync<T>(string moduleName, string exportedFunctionName, object[] args, bool allowRetry) public Task<T> InvokeExportAsync<T>(string moduleName, string exportedFunctionName, params object[] args)
{
return InvokeExportWithPossibleRetryAsync<T>(moduleName, exportedFunctionName, args, /* allowRetry */ true, CancellationToken.None);
}
public Task<T> InvokeExportAsync<T>(CancellationToken cancellationToken, string moduleName, string exportedFunctionName, params object[] args)
{
return InvokeExportWithPossibleRetryAsync<T>(moduleName, exportedFunctionName, args, /* allowRetry */ true, cancellationToken);
}
public async Task<T> InvokeExportWithPossibleRetryAsync<T>(string moduleName, string exportedFunctionName, object[] args, bool allowRetry, CancellationToken cancellationToken)
{ {
ThrowAnyOutstandingDelayedDisposalException(); ThrowAnyOutstandingDelayedDisposalException();
var nodeInstance = GetOrCreateCurrentNodeInstance(); var nodeInstance = GetOrCreateCurrentNodeInstance();
try try
{ {
return await nodeInstance.InvokeExportAsync<T>(moduleName, exportedFunctionName, args); return await nodeInstance.InvokeExportAsync<T>(cancellationToken, moduleName, exportedFunctionName, args);
} }
catch (NodeInvocationException ex) catch (NodeInvocationException ex)
{ {
@@ -71,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 // 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, // loop that masks an underlying problem. A newly-created Node instance should be able to accept invocations,
// or something more serious must be wrong. // or something more serious must be wrong.
return await InvokeExportWithPossibleRetryAsync<T>(moduleName, exportedFunctionName, args, allowRetry: false); return await InvokeExportWithPossibleRetryAsync<T>(moduleName, exportedFunctionName, args, /* allowRetry */ false, cancellationToken);
} }
else else
{ {

View File

@@ -43,15 +43,9 @@ In that case, you don't need to use NodeServices directly (or install it manuall
## For ASP.NET Core apps ## 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: Enable NodeServices in your application by first adding the following to your `ConfigureServices` method in `Startup.cs`:
```csharp
using Microsoft.AspNetCore.NodeServices;
```
... and then add to your `ConfigureServices` method in that file:
```csharp ```csharp
public void ConfigureServices(IServiceCollection services) public void ConfigureServices(IServiceCollection services)
@@ -90,18 +84,34 @@ If you want to put `addNumber.js` inside a subfolder rather than the root of you
## For non-ASP.NET apps ## 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 ```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
});
```
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<INodeServices>();
```
Or, if you want to obtain a separate (non-shared) `INodeServices` instance:
```csharp
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<T>(...)` etc. Besides this, the usage is the same as described for ASP.NET above, so you can now call `nodeServices.InvokeAsync<T>(...)` 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<T>` 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<T>` 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 # API Reference
@@ -111,15 +121,15 @@ You can dispose the `nodeServices` object whenever you are done with it (and it
```csharp ```csharp
AddNodeServices() AddNodeServices()
AddNodeServices(NodeServicesOptions options) AddNodeServices(Action<NodeServicesOptions> 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. 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 ```csharp
using Microsoft.AspNetCore.NodeServices; using Microsoft.Extensions.DependencyInjection;
``` ```
**Examples** **Examples**
@@ -133,23 +143,21 @@ services.AddNodeServices();
Or, specifying options: Or, specifying options:
```csharp ```csharp
services.AddNodeServices(new NodeServicesOptions services.AddNodeServices(options =>
{ {
WatchFileExtensions = new[] { ".coffee", ".sass" }, options.WatchFileExtensions = new[] { ".coffee", ".sass" };
// ... etc. - see other properties below // ... etc. - see other properties below
}); });
``` ```
**Parameters** **Parameters**
* `options` - type: `NodeServicesOptions` * `setupAction` - type: `Action<NodeServicesOptions>`
* Optional. If specified, configures how the `NodeServices` instances will work. * Optional. If not specified, defaults will be used.
* Properties: * Properties on `NodeServicesOptions`:
* `HostingModel` - an `NodeHostingModel` enum value. See: [hosting models](#hosting-models) * `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. * `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. * `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`.
If no `options` is passed, the default `WatchFileExtensions` 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. **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.
@@ -161,18 +169,16 @@ If no `options` is passed, the default `WatchFileExtensions` array includes `.js
CreateNodeServices(NodeServicesOptions options) CreateNodeServices(NodeServicesOptions options)
``` ```
Directly supplies an instance of `NodeServices` without using ASP.NET's DI system. Supplies a new (non-shared) instance of `NodeServices`.
**Example** **Example**
```csharp ```csharp
var nodeServices = Configuration.CreateNodeServices(new NodeServicesOptions { var options = new NodeServicesOptions(serviceProvider); // Obtains default options from DI config
HostingModel = NodeHostingModel.Socket var nodeServices = NodeServicesFactory.CreateNodeServices(options);
});
``` ```
**Parameters** **Parameters**
* `options` - type: `NodeServicesOptions`. * `options` - type: `NodeServicesOptions`.
* Configures the returned `NodeServices` instance. * Configures the returned `NodeServices` instance.
* Properties: * Properties:
@@ -327,12 +333,12 @@ People have asked about using [VroomJS](https://github.com/fogzot/vroomjs) as a
### Built-in hosting models ### 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 ```csharp
services.AddNodeServices(new NodeServicesOptions services.AddNodeServices(options =>
{ {
HostingModel = NodeHostingModel.Socket options.HostingModel = NodeHostingModel.Socket;
}); });
``` ```
@@ -349,12 +355,11 @@ The default transport may change from `Http` to `Socket` in the near future, bec
### Custom hosting models ### 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 ```csharp
var options = new NodeServicesOptions { services.AddNodeServices(options =>
NodeInstanceFactory = () => new MyCustomNodeInstance() {
}; options.NodeInstanceFactory = () => new MyCustomNodeInstance();
});
``` ```
Now you can pass this `options` object to [`AddNodeServices`](#addnodeservices) or [`CreateNodeServices`](#createnodeservices).

View File

@@ -4,6 +4,7 @@ import './Util/OverrideStdOutputs';
import * as http from 'http'; import * as http from 'http';
import * as path from 'path'; import * as path from 'path';
import { parseArgs } from './Util/ArgsUtil'; 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 // Webpack doesn't support dynamic requires for files not present at compile time, so grab a direct
// reference to Node's runtime 'require' function. // reference to Node's runtime 'require' function.
@@ -16,17 +17,19 @@ const server = http.createServer((req, res) => {
if (!hasSentResult) { if (!hasSentResult) {
hasSentResult = true; hasSentResult = true;
if (errorValue) { if (errorValue) {
res.statusCode = 500; respondWithError(res, errorValue);
if (errorValue.stack) {
res.end(errorValue.stack);
} else {
res.end(errorValue.toString());
}
} else if (typeof successValue !== 'string') { } else if (typeof successValue !== 'string') {
// Arbitrary object/number/etc - JSON-serialize it // 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.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify(successValue)); res.end(successValueJson);
} else { } else {
// String - can bypass JSON-serialization altogether // String - can bypass JSON-serialization altogether
res.setHeader('Content-Type', 'text/plain'); res.setHeader('Content-Type', 'text/plain');
@@ -73,9 +76,15 @@ server.listen(requestedPortOrZero, 'localhost', function () {
console.log('[Microsoft.AspNetCore.NodeServices:Listening]'); console.log('[Microsoft.AspNetCore.NodeServices:Listening]');
}); });
exitWhenParentExits(parseInt(parsedArgs.parentPid));
function readRequestBodyAsJson(request, callback) { function readRequestBodyAsJson(request, callback) {
let requestBodyAsString = ''; let requestBodyAsString = '';
request request.on('data', chunk => { requestBodyAsString += chunk; });
.on('data', chunk => { requestBodyAsString += chunk; }) request.on('end', () => { callback(JSON.parse(requestBodyAsString)); });
.on('end', () => { callback(JSON.parse(requestBodyAsString)); }); }
function respondWithError(res: http.ServerResponse, errorValue: any) {
res.statusCode = 500;
res.end(errorValue.stack || errorValue.toString());
} }

View File

@@ -6,6 +6,7 @@ import * as path from 'path';
import * as readline from 'readline'; import * as readline from 'readline';
import { Duplex } from 'stream'; import { Duplex } from 'stream';
import { parseArgs } from './Util/ArgsUtil'; import { parseArgs } from './Util/ArgsUtil';
import { exitWhenParentExits } from './Util/ExitWhenParentExits';
import * as virtualConnectionServer from './VirtualConnections/VirtualConnectionServer'; import * as virtualConnectionServer from './VirtualConnections/VirtualConnectionServer';
// Webpack doesn't support dynamic requires for files not present at compile time, so grab a direct // 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; const listenAddress = (useWindowsNamedPipes ? '\\\\.\\pipe\\' : '/tmp/') + parsedArgs.listenAddress;
server.listen(listenAddress); server.listen(listenAddress);
exitWhenParentExits(parseInt(parsedArgs.parentPid));
interface RpcInvocation { interface RpcInvocation {
moduleName: string; moduleName: string;
exportedFunctionName: string; exportedFunctionName: string;

View File

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

View File

@@ -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<T> OrThrowOnCancellation<T>(this Task<T> 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);
}
}
}

View File

@@ -5,12 +5,13 @@
"main": "main.js", "main": "main.js",
"typings": "main.d.ts", "typings": "main.d.ts",
"scripts": { "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" "test": "echo \"Error: no test specified\" && exit 1"
}, },
"author": "Microsoft", "author": "Microsoft",
"license": "Apache-2.0", "license": "Apache-2.0",
"devDependencies": { "devDependencies": {
"rimraf": "^2.5.4",
"typescript": "^1.8.10" "typescript": "^1.8.10"
} }
} }

View File

@@ -38,5 +38,5 @@ export interface Reducer<TState> extends Function {
} }
export interface ActionCreatorGeneric<TState> extends Function { export interface ActionCreatorGeneric<TState> extends Function {
(dispatch: Dispatch, getState: () => TState): any; (dispatch: Dispatch<TState>, getState: () => TState): any;
} }

View File

@@ -9,7 +9,7 @@
"defaultNamespace": "Microsoft.AspNetCore.ReactServices" "defaultNamespace": "Microsoft.AspNetCore.ReactServices"
}, },
"dependencies": { "dependencies": {
"Microsoft.AspNetCore.Mvc.TagHelpers": "1.0.0", "Microsoft.AspNetCore.Mvc.TagHelpers": "1.0.1",
"Microsoft.AspNetCore.SpaServices": "1.0.0-*" "Microsoft.AspNetCore.SpaServices": "1.0.0-*"
}, },
"frameworks": { "frameworks": {

View File

@@ -2,13 +2,11 @@ using System;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.NodeServices; using Microsoft.AspNetCore.NodeServices;
using Microsoft.AspNetCore.Razor.TagHelpers; using Microsoft.AspNetCore.Razor.TagHelpers;
using Microsoft.Extensions.PlatformAbstractions;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace Microsoft.AspNetCore.SpaServices.Prerendering namespace Microsoft.AspNetCore.SpaServices.Prerendering
@@ -20,6 +18,7 @@ namespace Microsoft.AspNetCore.SpaServices.Prerendering
private const string PrerenderExportAttributeName = "asp-prerender-export"; private const string PrerenderExportAttributeName = "asp-prerender-export";
private const string PrerenderWebpackConfigAttributeName = "asp-prerender-webpack-config"; private const string PrerenderWebpackConfigAttributeName = "asp-prerender-webpack-config";
private const string PrerenderDataAttributeName = "asp-prerender-data"; 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 static INodeServices _fallbackNodeServices; // Used only if no INodeServices was registered with DI
private readonly string _applicationBasePath; private readonly string _applicationBasePath;
@@ -35,10 +34,8 @@ namespace Microsoft.AspNetCore.SpaServices.Prerendering
// in your startup file, but then again it might be confusing that you don't need to. // in your startup file, but then again it might be confusing that you don't need to.
if (_nodeServices == null) if (_nodeServices == null)
{ {
_nodeServices = _fallbackNodeServices = Configuration.CreateNodeServices(new NodeServicesOptions _nodeServices = _fallbackNodeServices = NodeServicesFactory.CreateNodeServices(
{ new NodeServicesOptions(serviceProvider));
ProjectPath = _applicationBasePath
});
} }
} }
@@ -54,13 +51,28 @@ namespace Microsoft.AspNetCore.SpaServices.Prerendering
[HtmlAttributeName(PrerenderDataAttributeName)] [HtmlAttributeName(PrerenderDataAttributeName)]
public object CustomDataParameter { get; set; } public object CustomDataParameter { get; set; }
[HtmlAttributeName(PrerenderTimeoutAttributeName)]
public int TimeoutMillisecondsParameter { get; set; }
[HtmlAttributeNotBound] [HtmlAttributeNotBound]
[ViewContext] [ViewContext]
public ViewContext ViewContext { get; set; } public ViewContext ViewContext { get; set; }
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output) 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<IHttpRequestFeature>();
var unencodedPathAndQuery = requestFeature.RawTarget;
var request = ViewContext.HttpContext.Request; var request = ViewContext.HttpContext.Request;
var unencodedAbsoluteUrl = $"{request.Scheme}://{request.Host}{unencodedPathAndQuery}";
var result = await Prerenderer.RenderToString( var result = await Prerenderer.RenderToString(
_applicationBasePath, _applicationBasePath,
_nodeServices, _nodeServices,
@@ -69,9 +81,19 @@ namespace Microsoft.AspNetCore.SpaServices.Prerendering
ExportName = ExportName, ExportName = ExportName,
WebpackConfig = WebpackConfigPath WebpackConfig = WebpackConfigPath
}, },
request.GetEncodedUrl(), unencodedAbsoluteUrl,
request.Path + request.QueryString.Value, unencodedPathAndQuery,
CustomDataParameter); 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); output.Content.SetHtmlContent(result.Html);
// Also attach any specified globals to the 'window' object. This is useful for transferring // Also attach any specified globals to the 'window' object. This is useful for transferring

View File

@@ -23,7 +23,8 @@ namespace Microsoft.AspNetCore.SpaServices.Prerendering
JavaScriptModuleExport bootModule, JavaScriptModuleExport bootModule,
string requestAbsoluteUrl, string requestAbsoluteUrl,
string requestPathAndQuery, string requestPathAndQuery,
object customDataParameter) object customDataParameter,
int timeoutMilliseconds)
{ {
return nodeServices.InvokeExportAsync<RenderToStringResult>( return nodeServices.InvokeExportAsync<RenderToStringResult>(
NodeScript.Value.FileName, NodeScript.Value.FileName,
@@ -32,7 +33,8 @@ namespace Microsoft.AspNetCore.SpaServices.Prerendering
bootModule, bootModule,
requestAbsoluteUrl, requestAbsoluteUrl,
requestPathAndQuery, requestPathAndQuery,
customDataParameter); customDataParameter,
timeoutMilliseconds);
} }
} }
} }

View File

@@ -6,5 +6,6 @@ namespace Microsoft.AspNetCore.SpaServices.Prerendering
{ {
public JObject Globals { get; set; } public JObject Globals { get; set; }
public string Html { get; set; } public string Html { get; set; }
public string RedirectUrl { get; set; }
} }
} }

View File

@@ -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. 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. **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. 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 `<link rel='stylesheet'>` 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 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:
```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 `<link>` tag. For example, in `Views/Shared/_Layout.cshtml`, add:
```html
<link rel="stylesheet" href="~/dist/mystyles.css" asp-append-version="true" />
```
**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 ### 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: 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:

View File

@@ -40,12 +40,14 @@ namespace Microsoft.AspNetCore.Builder
// because it must *not* restart when files change (if it did, you'd lose all the benefits of Webpack // 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 // 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. // as fast as some theoretical future alternative.
var hostEnv = (IHostingEnvironment)appBuilder.ApplicationServices.GetService(typeof(IHostingEnvironment)); var nodeServicesOptions = new NodeServicesOptions(appBuilder.ApplicationServices);
var nodeServices = Configuration.CreateNodeServices(new NodeServicesOptions nodeServicesOptions.WatchFileExtensions = new string[] {}; // Don't watch anything
if (!string.IsNullOrEmpty(options.ProjectPath))
{ {
ProjectPath = hostEnv.ContentRootPath, nodeServicesOptions.ProjectPath = options.ProjectPath;
WatchFileExtensions = new string[] { } // Don't watch anything }
});
var nodeServices = NodeServicesFactory.CreateNodeServices(nodeServicesOptions);
// Get a filename matching the middleware Node script // Get a filename matching the middleware Node script
var script = EmbeddedResourceReader.Read(typeof(WebpackDevMiddleware), var script = EmbeddedResourceReader.Read(typeof(WebpackDevMiddleware),
@@ -55,7 +57,7 @@ namespace Microsoft.AspNetCore.Builder
// Tell Node to start the server hosting webpack-dev-middleware // Tell Node to start the server hosting webpack-dev-middleware
var devServerOptions = new var devServerOptions = new
{ {
webpackConfigPath = Path.Combine(hostEnv.ContentRootPath, options.ConfigFile ?? DefaultConfigFile), webpackConfigPath = Path.Combine(nodeServicesOptions.ProjectPath, options.ConfigFile ?? DefaultConfigFile),
suppliedOptions = options suppliedOptions = options
}; };
var devServerInfo = var devServerInfo =
@@ -79,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). // sees, not "localhost", so that it works even when you're not running on localhost (e.g., Docker).
appBuilder.Map(WebpackHotMiddlewareEndpoint, builder => appBuilder.Map(WebpackHotMiddlewareEndpoint, builder =>
{ {
builder.Use(next => async ctx => builder.Use(next => ctx =>
{ {
var hostname = ctx.Request.Host.Host; var hostname = ctx.Request.Host.Host;
ctx.Response.Redirect( ctx.Response.Redirect(
$"{WebpackDevMiddlewareScheme}://{hostname}:{devServerInfo.Port.ToString()}{WebpackHotMiddlewareEndpoint}"); $"{WebpackDevMiddlewareScheme}://{hostname}:{devServerInfo.Port.ToString()}{WebpackHotMiddlewareEndpoint}");
await Task.Yield(); return Task.FromResult(0);
}); });
}); });
} }

View File

@@ -6,5 +6,6 @@ namespace Microsoft.AspNetCore.SpaServices.Webpack
public int HotModuleReplacementServerPort { get; set; } public int HotModuleReplacementServerPort { get; set; }
public bool ReactHotModuleReplacement { get; set; } public bool ReactHotModuleReplacement { get; set; }
public string ConfigFile { get; set; } public string ConfigFile { get; set; }
public string ProjectPath { get; set; }
} }
} }

View File

@@ -1,10 +1,10 @@
{ {
"name": "aspnet-prerendering", "name": "aspnet-prerendering",
"version": "1.0.4", "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.", "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", "main": "index.js",
"scripts": { "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" "test": "echo \"Error: no test specified\" && exit 1"
}, },
"author": "Microsoft", "author": "Microsoft",
@@ -14,6 +14,7 @@
"es6-promise": "^3.1.2" "es6-promise": "^3.1.2"
}, },
"devDependencies": { "devDependencies": {
"rimraf": "^2.5.4",
"typescript": "^1.8.10" "typescript": "^1.8.10"
} }
} }

View File

@@ -5,6 +5,8 @@ import * as domain from 'domain';
import { run as domainTaskRun } from 'domain-task/main'; import { run as domainTaskRun } from 'domain-task/main';
import { baseUrl } from 'domain-task/fetch'; import { baseUrl } from 'domain-task/fetch';
const defaultTimeoutMilliseconds = 30 * 1000;
export interface RenderToStringCallback { export interface RenderToStringCallback {
(error: any, result: RenderToStringResult): void; (error: any, result: RenderToStringResult): void;
} }
@@ -33,7 +35,7 @@ export interface BootModuleInfo {
webpackConfig?: string; 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) => { findBootFunc(applicationBasePath, bootModule, (findBootFuncError, bootFunc) => {
if (findBootFuncError) { if (findBootFuncError) {
callback(findBootFuncError, null); callback(findBootFuncError, null);
@@ -66,9 +68,23 @@ export function renderToString(callback: RenderToStringCallback, applicationBase
// Make the base URL available to the 'domain-tasks/fetch' helper within this execution context // Make the base URL available to the 'domain-tasks/fetch' helper within this execution context
baseUrl(absoluteRequestUrl); 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 // Actually perform the rendering
bootFunc(params).then(successResult => { bootFuncPromiseWithTimeout.then(successResult => {
callback(null, { html: successResult.html, globals: successResult.globals }); callback(null, successResult);
}, error => { }, error => {
callback(error, null); callback(error, null);
}); });
@@ -84,6 +100,25 @@ export function renderToString(callback: RenderToStringCallback, applicationBase
}); });
} }
function wrapWithTimeout<T>(promise: Promise<T>, timeoutMilliseconds: number, timeoutRejectionValue: any): Promise<T> {
return new Promise<T>((resolve, reject) => {
const timeoutTimer = setTimeout(() => {
reject(timeoutRejectionValue);
}, timeoutMilliseconds);
promise.then(
resolvedValue => {
clearTimeout(timeoutTimer);
resolve(resolvedValue);
},
rejectedValue => {
clearTimeout(timeoutTimer);
reject(rejectedValue);
}
)
});
}
function findBootModule<T>(applicationBasePath: string, bootModule: BootModuleInfo, callback: (error: any, foundModule: T) => void) { function findBootModule<T>(applicationBasePath: string, bootModule: BootModuleInfo, callback: (error: any, foundModule: T) => void) {
const bootModuleNameFullPath = path.resolve(applicationBasePath, bootModule.moduleName); const bootModuleNameFullPath = path.resolve(applicationBasePath, bootModule.moduleName);
if (bootModule.webpackConfig) { if (bootModule.webpackConfig) {

View File

@@ -1,10 +1,10 @@
{ {
"name": "aspnet-webpack-react", "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.", "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", "main": "index.js",
"scripts": { "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" "test": "echo \"Error: no test specified\" && exit 1"
}, },
"author": "Microsoft", "author": "Microsoft",
@@ -15,11 +15,12 @@
"babel-plugin-react-transform": "^2.0.2", "babel-plugin-react-transform": "^2.0.2",
"babel-preset-es2015": "^6.6.0", "babel-preset-es2015": "^6.6.0",
"babel-preset-react": "^6.5.0", "babel-preset-react": "^6.5.0",
"react": "^0.14.7", "react": "^15.0.0",
"react-transform-hmr": "^1.0.4", "react-transform-hmr": "^1.0.4",
"webpack": "^1.12.14" "webpack": "^1.12.14"
}, },
"devDependencies": { "devDependencies": {
"rimraf": "^2.5.4",
"typescript": "^1.8.10" "typescript": "^1.8.10"
} }
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "aspnet-webpack", "name": "aspnet-webpack",
"version": "1.0.9", "version": "1.0.11",
"description": "Helpers for using Webpack in ASP.NET Core projects. Works in conjunction with the Microsoft.AspNetCore.SpaServices NuGet package.", "description": "Helpers for using Webpack in ASP.NET Core projects. Works in conjunction with the Microsoft.AspNetCore.SpaServices NuGet package.",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
@@ -14,12 +14,15 @@
"connect": "^3.4.1", "connect": "^3.4.1",
"memory-fs": "^0.3.0", "memory-fs": "^0.3.0",
"require-from-string": "^1.1.0", "require-from-string": "^1.1.0",
"webpack": "^1.12.14", "webpack-dev-middleware": "^1.6.1",
"webpack-dev-middleware": "^1.5.1",
"webpack-externals-plugin": "^1.0.0" "webpack-externals-plugin": "^1.0.0"
}, },
"devDependencies": { "devDependencies": {
"typescript": "^1.8.10", "tsd": "0.6.5",
"rimraf": "^2.5.2" "rimraf": "^2.5.4",
"typescript": "^1.8.10"
},
"peerDependencies": {
"webpack": "^1.13.2"
} }
} }

View File

@@ -43,12 +43,26 @@ export function createWebpackDevServer(callback: CreateDevServerCallback, option
const listener = app.listen(suggestedHMRPortOrZero, () => { const listener = app.listen(suggestedHMRPortOrZero, () => {
// Build the final Webpack config based on supplied options // Build the final Webpack config based on supplied options
if (enableHotModuleReplacement) { if (enableHotModuleReplacement) {
// TODO: Stop assuming there's an entry point called 'main' // For this, we only support the key/value config format, not string or string[], since
if (typeof webpackConfig.entry['main'] === 'string') { // those ones don't clearly indicate what the resulting bundle name will be
webpackConfig.entry['main'] = ['webpack-hot-middleware/client', webpackConfig.entry['main']]; const entryPoints = webpackConfig.entry;
} else { const isObjectStyleConfig = entryPoints
webpackConfig.entry['main'].unshift('webpack-hot-middleware/client'); && 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( webpackConfig.plugins.push(
new webpack.HotModuleReplacementPlugin() new webpack.HotModuleReplacementPlugin()
); );

View File

@@ -4,7 +4,7 @@
"description": "Tracks outstanding operations for a logical thread of execution", "description": "Tracks outstanding operations for a logical thread of execution",
"main": "index.js", "main": "index.js",
"scripts": { "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" "test": "echo \"Error: no test specified\" && exit 1"
}, },
"author": "Microsoft", "author": "Microsoft",
@@ -14,6 +14,7 @@
"isomorphic-fetch": "^2.2.1" "isomorphic-fetch": "^2.2.1"
}, },
"devDependencies": { "devDependencies": {
"rimraf": "^2.5.4",
"typescript": "^1.8.10" "typescript": "^1.8.10"
} }
} }

View File

@@ -8,7 +8,7 @@
"Microsoft" "Microsoft"
], ],
"dependencies": { "dependencies": {
"Microsoft.AspNetCore.Mvc": "1.0.0", "Microsoft.AspNetCore.Mvc": "1.0.1",
"Microsoft.AspNetCore.NodeServices": "1.0.0-*" "Microsoft.AspNetCore.NodeServices": "1.0.0-*"
}, },
"frameworks": { "frameworks": {

View File

@@ -5,16 +5,16 @@
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath> <VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked> <TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
</PropertyGroup> </PropertyGroup>
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.Props" Condition="'$(VSToolsPath)' != ''" /> <Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.Props" Condition="'$(VSToolsPath)' != ''" />
<PropertyGroup Label="Globals"> <PropertyGroup Label="Globals">
<ProjectGuid>8f5cb8a9-3086-4b49-a1c2-32a9f89bca11</ProjectGuid> <ProjectGuid>8f5cb8a9-3086-4b49-a1c2-32a9f89bca11</ProjectGuid>
<RootNamespace>Angular2Spa</RootNamespace> <RootNamespace>Angular2Spa</RootNamespace>
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">..\..\..\JavaScriptServices.sln\artifacts\obj\$(MSBuildProjectName)</BaseIntermediateOutputPath> <BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">.\obj</BaseIntermediateOutputPath>
<OutputPath Condition="'$(OutputPath)'=='' ">.\bin\</OutputPath> <OutputPath Condition="'$(OutputPath)'=='' ">.\bin\</OutputPath>
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
<SchemaVersion>2.0</SchemaVersion> <SchemaVersion>2.0</SchemaVersion>
<DevelopmentServerPort>2018</DevelopmentServerPort>
</PropertyGroup> </PropertyGroup>
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.targets" Condition="'$(VSToolsPath)' != ''" /> <Import Project="$(VSToolsPath)\DotNet.Web\Microsoft.DotNet.Web.targets" Condition="'$(VSToolsPath)' != ''" />
</Project> </Project>

View File

@@ -1,23 +1,6 @@
{ {
"name": "WebApplicationBasic", "name": "WebApplicationBasic",
"version": "0.0.0", "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": { "dependencies": {
"@angular/common": "2.0.0-rc.4", "@angular/common": "2.0.0-rc.4",
"@angular/compiler": "2.0.0-rc.4", "@angular/compiler": "2.0.0-rc.4",
@@ -29,12 +12,27 @@
"@angular/router": "3.0.0-beta.2", "@angular/router": "3.0.0-beta.2",
"angular2-universal": "^0.104.5", "angular2-universal": "^0.104.5",
"aspnet-prerendering": "^1.0.2", "aspnet-prerendering": "^1.0.2",
"aspnet-webpack": "^1.0.6",
"bootstrap": "^3.3.6",
"css": "^2.2.1", "css": "^2.2.1",
"css-loader": "^0.23.1",
"es6-shim": "^0.35.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", "isomorphic-fetch": "^2.2.1",
"jquery": "^2.2.1",
"preboot": "^2.0.10", "preboot": "^2.0.10",
"raw-loader": "^0.5.1",
"rxjs": "5.0.0-beta.6", "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-externals-plugin": "^1.0.0",
"webpack-hot-middleware": "^2.10.0",
"zone.js": "^0.6.12" "zone.js": "^0.6.12"
} }
} }

View File

@@ -1,18 +1,18 @@
{ {
"dependencies": { "dependencies": {
"Microsoft.NETCore.App": { "Microsoft.NETCore.App": {
"version": "1.0.0", "version": "1.0.1",
"type": "platform" "type": "platform"
}, },
"Microsoft.AspNetCore.AngularServices": "1.0.0-*", "Microsoft.AspNetCore.AngularServices": "1.0.0-*",
"Microsoft.AspNetCore.Diagnostics": "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": { "Microsoft.AspNetCore.Razor.Tools": {
"version": "1.0.0-preview2-final", "version": "1.0.0-preview2-final",
"type": "build" "type": "build"
}, },
"Microsoft.AspNetCore.Server.IISIntegration": "1.0.0", "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.AspNetCore.StaticFiles": "1.0.0",
"Microsoft.Extensions.Configuration.EnvironmentVariables": "1.0.0", "Microsoft.Extensions.Configuration.EnvironmentVariables": "1.0.0",
"Microsoft.Extensions.Configuration.Json": "1.0.0", "Microsoft.Extensions.Configuration.Json": "1.0.0",

View File

@@ -184,9 +184,12 @@ ClientBin/
*.dbproj.schemaview *.dbproj.schemaview
*.pfx *.pfx
*.publishsettings *.publishsettings
node_modules/
orleans.codegen.cs orleans.codegen.cs
# Workaround for https://github.com/aspnet/JavaScriptServices/issues/235
/node_modules/**
!/node_modules/_placeholder.txt
# RIA/Silverlight projects # RIA/Silverlight projects
Generated_Code/ Generated_Code/

View File

@@ -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.

View File

@@ -8,6 +8,7 @@
"skipDefaultLibCheck": true "skipDefaultLibCheck": true
}, },
"exclude": [ "exclude": [
"bin",
"node_modules" "node_modules"
] ]
} }

View File

@@ -29,6 +29,8 @@ module.exports = {
'@angular/platform-browser-dynamic', '@angular/platform-browser-dynamic',
'@angular/router', '@angular/router',
'@angular/platform-server', '@angular/platform-server',
'reflect-metadata',
'zone.js',
] ]
}, },
output: { output: {

View File

@@ -1,4 +1,5 @@
import * as ko from 'knockout'; import * as ko from 'knockout';
import 'isomorphic-fetch';
interface WeatherForecast { interface WeatherForecast {
dateFormatted: string; dateFormatted: string;

View File

@@ -1,5 +1,5 @@
import * as ko from 'knockout'; import * as ko from 'knockout';
import * as crossroads from 'crossroads'; import crossroads = require('crossroads');
// This module configures crossroads.js, a routing library. If you prefer, you // 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 // can use any other routing library (or none at all) as Knockout is designed to

View File

@@ -5,16 +5,16 @@
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath> <VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked> <TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
</PropertyGroup> </PropertyGroup>
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.Props" Condition="'$(VSToolsPath)' != ''" /> <Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.Props" Condition="'$(VSToolsPath)' != ''" />
<PropertyGroup Label="Globals"> <PropertyGroup Label="Globals">
<ProjectGuid>85231b41-6998-49ae-abd2-5124c83dbef2</ProjectGuid> <ProjectGuid>85231b41-6998-49ae-abd2-5124c83dbef2</ProjectGuid>
<RootNamespace>KnockoutSpa</RootNamespace> <RootNamespace>KnockoutSpa</RootNamespace>
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">..\..\..\JavaScriptServices.sln\artifacts\obj\$(MSBuildProjectName)</BaseIntermediateOutputPath> <BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">.\obj</BaseIntermediateOutputPath>
<OutputPath Condition="'$(OutputPath)'=='' ">..\..\..\JavaScriptServices.sln\artifacts\bin\$(MSBuildProjectName)\</OutputPath> <OutputPath Condition="'$(OutputPath)'=='' ">.\bin\</OutputPath>
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
<SchemaVersion>2.0</SchemaVersion> <SchemaVersion>2.0</SchemaVersion>
<DevelopmentServerPort>2018</DevelopmentServerPort>
</PropertyGroup> </PropertyGroup>
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.targets" Condition="'$(VSToolsPath)' != ''" /> <Import Project="$(VSToolsPath)\DotNet.Web\Microsoft.DotNet.Web.targets" Condition="'$(VSToolsPath)' != ''" />
</Project> </Project>

View File

@@ -11,6 +11,7 @@
"extract-text-webpack-plugin": "^1.0.1", "extract-text-webpack-plugin": "^1.0.1",
"file-loader": "^0.8.5", "file-loader": "^0.8.5",
"history": "^2.0.1", "history": "^2.0.1",
"isomorphic-fetch": "^2.2.1",
"jquery": "^2.2.1", "jquery": "^2.2.1",
"knockout": "^3.4.0", "knockout": "^3.4.0",
"raw-loader": "^0.5.1", "raw-loader": "^0.5.1",
@@ -18,9 +19,7 @@
"ts-loader": "^0.8.1", "ts-loader": "^0.8.1",
"typescript": "^1.8.2", "typescript": "^1.8.2",
"url-loader": "^0.5.7", "url-loader": "^0.5.7",
"webpack": "^1.12.14" "webpack": "^1.12.14",
},
"dependencies": {
"webpack-hot-middleware": "^2.10.0" "webpack-hot-middleware": "^2.10.0"
} }
} }

View File

@@ -1,18 +1,18 @@
{ {
"dependencies": { "dependencies": {
"Microsoft.NETCore.App": { "Microsoft.NETCore.App": {
"version": "1.0.0", "version": "1.0.1",
"type": "platform" "type": "platform"
}, },
"Microsoft.AspNetCore.SpaServices": "1.0.0-*", "Microsoft.AspNetCore.SpaServices": "1.0.0-*",
"Microsoft.AspNetCore.Diagnostics": "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": { "Microsoft.AspNetCore.Razor.Tools": {
"version": "1.0.0-preview2-final", "version": "1.0.0-preview2-final",
"type": "build" "type": "build"
}, },
"Microsoft.AspNetCore.Server.IISIntegration": "1.0.0", "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.AspNetCore.StaticFiles": "1.0.0",
"Microsoft.Extensions.Configuration.EnvironmentVariables": "1.0.0", "Microsoft.Extensions.Configuration.EnvironmentVariables": "1.0.0",
"Microsoft.Extensions.Configuration.Json": "1.0.0", "Microsoft.Extensions.Configuration.Json": "1.0.0",

View File

@@ -184,9 +184,12 @@ ClientBin/
*.dbproj.schemaview *.dbproj.schemaview
*.pfx *.pfx
*.publishsettings *.publishsettings
node_modules/
orleans.codegen.cs orleans.codegen.cs
# Workaround for https://github.com/aspnet/JavaScriptServices/issues/235
/node_modules/**
!/node_modules/_placeholder.txt
# RIA/Silverlight projects # RIA/Silverlight projects
Generated_Code/ Generated_Code/

View File

@@ -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.

View File

@@ -6,6 +6,7 @@
"skipDefaultLibCheck": true "skipDefaultLibCheck": true
}, },
"exclude": [ "exclude": [
"bin",
"node_modules" "node_modules"
] ]
} }

View File

@@ -5,9 +5,6 @@
"path": "typings", "path": "typings",
"bundle": "typings/tsd.d.ts", "bundle": "typings/tsd.d.ts",
"installed": { "installed": {
"whatwg-fetch/whatwg-fetch.d.ts": {
"commit": "dade4414712ce84e3c63393f1aae407e9e7e6af7"
},
"knockout/knockout.d.ts": { "knockout/knockout.d.ts": {
"commit": "9f0f926a12026287b5a4a229e5672c01e7549313" "commit": "9f0f926a12026287b5a4a229e5672c01e7549313"
}, },
@@ -28,6 +25,9 @@
}, },
"js-signals/js-signals.d.ts": { "js-signals/js-signals.d.ts": {
"commit": "9f0f926a12026287b5a4a229e5672c01e7549313" "commit": "9f0f926a12026287b5a4a229e5672c01e7549313"
},
"isomorphic-fetch/isomorphic-fetch.d.ts": {
"commit": "57ec5fbb76060329c10959d449eb1d4e70b15a65"
} }
} }
} }

View File

@@ -0,0 +1,119 @@
// Type definitions for isomorphic-fetch
// Project: https://github.com/matthew-andrews/isomorphic-fetch
// Definitions by: Todd Lucas <https://github.com/toddlucas>
// 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<string>;
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<string>;
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<string>;
has(name: string): boolean;
set(name: string, value: string): void;
}
interface IBody {
bodyUsed: boolean;
arrayBuffer(): Promise<ArrayBuffer>;
blob(): Promise<Blob>;
formData(): Promise<FormData>;
json(): Promise<any>;
json<T>(): Promise<T>;
text(): Promise<string>;
}
declare class Body implements IBody {
bodyUsed: boolean;
arrayBuffer(): Promise<ArrayBuffer>;
blob(): Promise<Blob>;
formData(): Promise<FormData>;
json(): Promise<any>;
json<T>(): Promise<T>;
text(): Promise<string>;
}
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<IResponse>;
}
declare var fetch: IFetchStatic;
declare module "isomorphic-fetch" {
export = fetch;
}

View File

@@ -1,8 +1,8 @@
/// <reference path="es6-promise/es6-promise.d.ts" /> /// <reference path="es6-promise/es6-promise.d.ts" />
/// <reference path="knockout/knockout.d.ts" /> /// <reference path="knockout/knockout.d.ts" />
/// <reference path="requirejs/require.d.ts" /> /// <reference path="requirejs/require.d.ts" />
/// <reference path="whatwg-fetch/whatwg-fetch.d.ts" />
/// <reference path="history/history.d.ts" /> /// <reference path="history/history.d.ts" />
/// <reference path="react-router/history.d.ts" /> /// <reference path="react-router/history.d.ts" />
/// <reference path="crossroads/crossroads.d.ts" /> /// <reference path="crossroads/crossroads.d.ts" />
/// <reference path="js-signals/js-signals.d.ts" /> /// <reference path="js-signals/js-signals.d.ts" />
/// <reference path="isomorphic-fetch/isomorphic-fetch.d.ts" />

View File

@@ -1,85 +0,0 @@
// Type definitions for fetch API
// Project: https://github.com/github/fetch
// Definitions by: Ryan Graham <https://github.com/ryan-codingintrigue>
// 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<string>;
has(name: string): boolean;
set(name: string, value: string): void;
}
declare class Body {
bodyUsed: boolean;
arrayBuffer(): Promise<ArrayBuffer>;
blob(): Promise<Blob>;
formData(): Promise<FormData>;
json(): Promise<any>;
json<T>(): Promise<T>;
text(): Promise<string>;
}
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<string>;
declare type BodyInit = Blob|FormData|string;
declare type RequestInfo = Request|string;
interface Window {
fetch(url: string|Request, init?: RequestInit): Promise<Response>;
}
declare var fetch: typeof window.fetch;

View File

@@ -15,7 +15,7 @@ module.exports = {
] ]
}, },
entry: { 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: { output: {
path: path.join(__dirname, 'wwwroot', 'dist'), path: path.join(__dirname, 'wwwroot', 'dist'),

View File

@@ -5,15 +5,22 @@ import { match, RouterContext } from 'react-router';
import createMemoryHistory from 'history/lib/createMemoryHistory'; import createMemoryHistory from 'history/lib/createMemoryHistory';
import routes from './routes'; import routes from './routes';
import configureStore from './configureStore'; import configureStore from './configureStore';
type BootResult = { html?: string, globals?: { [key: string]: any }, redirectUrl?: string};
export default function (params: any): Promise<{ html: string }> { export default function (params: any): Promise<{ html: string }> {
return new Promise<{ html: string, globals: { [key: string]: any } }>((resolve, reject) => { return new Promise<BootResult>((resolve, reject) => {
// Match the incoming request against the list of client-side routes // Match the incoming request against the list of client-side routes
match({ routes, location: params.location }, (error, redirectLocation, renderProps: any) => { match({ routes, location: params.location }, (error, redirectLocation, renderProps: any) => {
if (error) { if (error) {
throw 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 it didn't match any route, renderProps will be undefined
if (!renderProps) { if (!renderProps) {
throw new Error(`The location '${ params.url }' doesn't match any route configured in react-router.`); throw new Error(`The location '${ params.url }' doesn't match any route configured in react-router.`);

View File

@@ -5,16 +5,16 @@
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath> <VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked> <TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
</PropertyGroup> </PropertyGroup>
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.Props" Condition="'$(VSToolsPath)' != ''" /> <Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.Props" Condition="'$(VSToolsPath)' != ''" />
<PropertyGroup Label="Globals"> <PropertyGroup Label="Globals">
<ProjectGuid>dbfc6db0-a6d1-4694-a108-1c604b988da3</ProjectGuid> <ProjectGuid>dbfc6db0-a6d1-4694-a108-1c604b988da3</ProjectGuid>
<RootNamespace>ReactReduxSpa</RootNamespace> <RootNamespace>ReactReduxSpa</RootNamespace>
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">..\..\..\JavaScriptServices.sln\artifacts\obj\$(MSBuildProjectName)</BaseIntermediateOutputPath> <BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">.\obj</BaseIntermediateOutputPath>
<OutputPath Condition="'$(OutputPath)'=='' ">.\bin\</OutputPath> <OutputPath Condition="'$(OutputPath)'=='' ">.\bin\</OutputPath>
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
<SchemaVersion>2.0</SchemaVersion> <SchemaVersion>2.0</SchemaVersion>
<DevelopmentServerPort>2018</DevelopmentServerPort>
</PropertyGroup> </PropertyGroup>
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.targets" Condition="'$(VSToolsPath)' != ''" /> <Import Project="$(VSToolsPath)\DotNet.Web\Microsoft.DotNet.Web.targets" Condition="'$(VSToolsPath)' != ''" />
</Project> </Project>

View File

@@ -1,29 +1,21 @@
{ {
"name": "WebApplicationBasic", "name": "WebApplicationBasic",
"version": "0.0.0", "version": "0.0.0",
"devDependencies": { "dependencies": {
"aspnet-prerendering": "^1.0.2",
"aspnet-webpack": "^1.0.6", "aspnet-webpack": "^1.0.6",
"aspnet-webpack-react": "^1.0.1", "aspnet-webpack-react": "^1.0.2",
"babel-core": "^6.5.2",
"babel-loader": "^6.2.3", "babel-loader": "^6.2.3",
"babel-preset-es2015": "^6.5.0", "babel-preset-es2015": "^6.5.0",
"babel-preset-react": "^6.5.0", "babel-preset-react": "^6.5.0",
"bootstrap": "^3.3.6", "bootstrap": "^3.3.6",
"css-loader": "^0.23.1", "css-loader": "^0.23.1",
"domain-task": "^2.0.0",
"extendify": "^1.0.0", "extendify": "^1.0.0",
"extract-text-webpack-plugin": "^1.0.1", "extract-text-webpack-plugin": "^1.0.1",
"file-loader": "^0.8.5", "file-loader": "^0.8.5",
"jquery": "^2.2.1", "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": "^15.0.1",
"react-dom": "^15.0.1", "react-dom": "^15.0.1",
"react-redux": "^4.4.4", "react-redux": "^4.4.4",
@@ -32,6 +24,12 @@
"redux": "^3.4.0", "redux": "^3.4.0",
"redux-thunk": "^2.0.1", "redux-thunk": "^2.0.1",
"redux-typed": "^1.0.0", "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"
} }
} }

View File

@@ -1,18 +1,18 @@
{ {
"dependencies": { "dependencies": {
"Microsoft.NETCore.App": { "Microsoft.NETCore.App": {
"version": "1.0.0", "version": "1.0.1",
"type": "platform" "type": "platform"
}, },
"Microsoft.AspNetCore.ReactServices": "1.0.0-*", "Microsoft.AspNetCore.ReactServices": "1.0.0-*",
"Microsoft.AspNetCore.Diagnostics": "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": { "Microsoft.AspNetCore.Razor.Tools": {
"version": "1.0.0-preview2-final", "version": "1.0.0-preview2-final",
"type": "build" "type": "build"
}, },
"Microsoft.AspNetCore.Server.IISIntegration": "1.0.0", "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.AspNetCore.StaticFiles": "1.0.0",
"Microsoft.Extensions.Configuration.EnvironmentVariables": "1.0.0", "Microsoft.Extensions.Configuration.EnvironmentVariables": "1.0.0",
"Microsoft.Extensions.Configuration.Json": "1.0.0", "Microsoft.Extensions.Configuration.Json": "1.0.0",

View File

@@ -184,9 +184,12 @@ ClientBin/
*.dbproj.schemaview *.dbproj.schemaview
*.pfx *.pfx
*.publishsettings *.publishsettings
node_modules/
orleans.codegen.cs orleans.codegen.cs
# Workaround for https://github.com/aspnet/JavaScriptServices/issues/235
/node_modules/**
!/node_modules/_placeholder.txt
# RIA/Silverlight projects # RIA/Silverlight projects
Generated_Code/ Generated_Code/

View File

@@ -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.

View File

@@ -8,6 +8,7 @@
"skipDefaultLibCheck": true "skipDefaultLibCheck": true
}, },
"exclude": [ "exclude": [
"bin",
"node_modules" "node_modules"
] ]
} }

View File

@@ -1,4 +1,5 @@
import * as React from 'react'; import * as React from 'react';
import 'isomorphic-fetch';
interface FetchDataExampleState { interface FetchDataExampleState {
forecasts: WeatherForecast[]; forecasts: WeatherForecast[];

View File

@@ -5,16 +5,16 @@
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath> <VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked> <TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
</PropertyGroup> </PropertyGroup>
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.Props" Condition="'$(VSToolsPath)' != ''" /> <Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.Props" Condition="'$(VSToolsPath)' != ''" />
<PropertyGroup Label="Globals"> <PropertyGroup Label="Globals">
<ProjectGuid>e9d1a695-f0e6-46f2-b5e3-72f4af805387</ProjectGuid> <ProjectGuid>e9d1a695-f0e6-46f2-b5e3-72f4af805387</ProjectGuid>
<RootNamespace>ReactSpa</RootNamespace> <RootNamespace>ReactSpa</RootNamespace>
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">..\..\..\JavaScriptServices.sln\artifacts\obj\$(MSBuildProjectName)</BaseIntermediateOutputPath> <BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">.\obj</BaseIntermediateOutputPath>
<OutputPath Condition="'$(OutputPath)'=='' ">.\bin\</OutputPath> <OutputPath Condition="'$(OutputPath)'=='' ">.\bin\</OutputPath>
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
<SchemaVersion>2.0</SchemaVersion> <SchemaVersion>2.0</SchemaVersion>
<DevelopmentServerPort>2018</DevelopmentServerPort>
</PropertyGroup> </PropertyGroup>
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.targets" Condition="'$(VSToolsPath)' != ''" /> <Import Project="$(VSToolsPath)\DotNet.Web\Microsoft.DotNet.Web.targets" Condition="'$(VSToolsPath)' != ''" />
</Project> </Project>

View File

@@ -3,7 +3,8 @@
"version": "0.0.0", "version": "0.0.0",
"devDependencies": { "devDependencies": {
"aspnet-webpack": "^1.0.6", "aspnet-webpack": "^1.0.6",
"aspnet-webpack-react": "^1.0.0", "aspnet-webpack-react": "^1.0.2",
"babel-core": "^6.5.2",
"babel-loader": "^6.2.3", "babel-loader": "^6.2.3",
"babel-preset-es2015": "^6.5.0", "babel-preset-es2015": "^6.5.0",
"babel-preset-react": "^6.5.0", "babel-preset-react": "^6.5.0",
@@ -12,18 +13,16 @@
"extendify": "^1.0.0", "extendify": "^1.0.0",
"extract-text-webpack-plugin": "^1.0.1", "extract-text-webpack-plugin": "^1.0.1",
"file-loader": "^0.8.5", "file-loader": "^0.8.5",
"isomorphic-fetch": "^2.2.1",
"jquery": "^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", "style-loader": "^0.13.0",
"ts-loader": "^0.8.1", "ts-loader": "^0.8.1",
"typescript": "^1.8.2", "typescript": "^1.8.2",
"url-loader": "^0.5.7", "url-loader": "^0.5.7",
"webpack": "^1.12.14", "webpack": "^1.12.14",
"webpack-hot-middleware": "^2.10.0" "webpack-hot-middleware": "^2.10.0"
},
"dependencies": {
"babel-core": "^6.5.2",
"react": "^15.0.1",
"react-dom": "^15.0.1",
"react-router": "^2.1.1"
} }
} }

View File

@@ -1,18 +1,18 @@
{ {
"dependencies": { "dependencies": {
"Microsoft.NETCore.App": { "Microsoft.NETCore.App": {
"version": "1.0.0", "version": "1.0.1",
"type": "platform" "type": "platform"
}, },
"Microsoft.AspNetCore.ReactServices": "1.0.0-*", "Microsoft.AspNetCore.ReactServices": "1.0.0-*",
"Microsoft.AspNetCore.Diagnostics": "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": { "Microsoft.AspNetCore.Razor.Tools": {
"version": "1.0.0-preview2-final", "version": "1.0.0-preview2-final",
"type": "build" "type": "build"
}, },
"Microsoft.AspNetCore.Server.IISIntegration": "1.0.0", "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.AspNetCore.StaticFiles": "1.0.0",
"Microsoft.Extensions.Configuration.EnvironmentVariables": "1.0.0", "Microsoft.Extensions.Configuration.EnvironmentVariables": "1.0.0",
"Microsoft.Extensions.Configuration.Json": "1.0.0", "Microsoft.Extensions.Configuration.Json": "1.0.0",

View File

@@ -184,9 +184,12 @@ ClientBin/
*.dbproj.schemaview *.dbproj.schemaview
*.pfx *.pfx
*.publishsettings *.publishsettings
node_modules/
orleans.codegen.cs orleans.codegen.cs
# Workaround for https://github.com/aspnet/JavaScriptServices/issues/235
/node_modules/**
!/node_modules/_placeholder.txt
# RIA/Silverlight projects # RIA/Silverlight projects
Generated_Code/ Generated_Code/

View File

@@ -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.

View File

@@ -7,6 +7,7 @@
"skipDefaultLibCheck": true "skipDefaultLibCheck": true
}, },
"exclude": [ "exclude": [
"bin",
"node_modules" "node_modules"
] ]
} }

View File

@@ -17,8 +17,8 @@
"react-router/history.d.ts": { "react-router/history.d.ts": {
"commit": "dade4414712ce84e3c63393f1aae407e9e7e6af7" "commit": "dade4414712ce84e3c63393f1aae407e9e7e6af7"
}, },
"whatwg-fetch/whatwg-fetch.d.ts": { "isomorphic-fetch/isomorphic-fetch.d.ts": {
"commit": "dade4414712ce84e3c63393f1aae407e9e7e6af7" "commit": "57ec5fbb76060329c10959d449eb1d4e70b15a65"
} }
} }
} }

View File

@@ -0,0 +1,119 @@
// Type definitions for isomorphic-fetch
// Project: https://github.com/matthew-andrews/isomorphic-fetch
// Definitions by: Todd Lucas <https://github.com/toddlucas>
// 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<string>;
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<string>;
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<string>;
has(name: string): boolean;
set(name: string, value: string): void;
}
interface IBody {
bodyUsed: boolean;
arrayBuffer(): Promise<ArrayBuffer>;
blob(): Promise<Blob>;
formData(): Promise<FormData>;
json(): Promise<any>;
json<T>(): Promise<T>;
text(): Promise<string>;
}
declare class Body implements IBody {
bodyUsed: boolean;
arrayBuffer(): Promise<ArrayBuffer>;
blob(): Promise<Blob>;
formData(): Promise<FormData>;
json(): Promise<any>;
json<T>(): Promise<T>;
text(): Promise<string>;
}
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<IResponse>;
}
declare var fetch: IFetchStatic;
declare module "isomorphic-fetch" {
export = fetch;
}

View File

@@ -3,4 +3,4 @@
/// <reference path="react-router/history.d.ts" /> /// <reference path="react-router/history.d.ts" />
/// <reference path="react-router/react-router.d.ts" /> /// <reference path="react-router/react-router.d.ts" />
/// <reference path="react/react-dom.d.ts" /> /// <reference path="react/react-dom.d.ts" />
/// <reference path="whatwg-fetch/whatwg-fetch.d.ts" /> /// <reference path="isomorphic-fetch/isomorphic-fetch.d.ts" />

View File

@@ -1,85 +0,0 @@
// Type definitions for fetch API
// Project: https://github.com/github/fetch
// Definitions by: Ryan Graham <https://github.com/ryan-codingintrigue>
// 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<string>;
has(name: string): boolean;
set(name: string, value: string): void;
}
declare class Body {
bodyUsed: boolean;
arrayBuffer(): Promise<ArrayBuffer>;
blob(): Promise<Blob>;
formData(): Promise<FormData>;
json(): Promise<any>;
json<T>(): Promise<T>;
text(): Promise<string>;
}
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<string>;
declare type BodyInit = Blob|FormData|string;
declare type RequestInfo = Request|string;
interface Window {
fetch(url: string|Request, init?: RequestInit): Promise<Response>;
}
declare var fetch: typeof window.fetch;

View File

@@ -15,7 +15,7 @@ module.exports = {
] ]
}, },
entry: { 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: { output: {
path: path.join(__dirname, 'wwwroot', 'dist'), path: path.join(__dirname, 'wwwroot', 'dist'),

View File

@@ -5,16 +5,16 @@
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath> <VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked> <TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
</PropertyGroup> </PropertyGroup>
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.Props" Condition="'$(VSToolsPath)' != ''" /> <Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.Props" Condition="'$(VSToolsPath)' != ''" />
<PropertyGroup Label="Globals"> <PropertyGroup Label="Globals">
<ProjectGuid>cb4398d6-b7f1-449a-ae02-828769679232</ProjectGuid> <ProjectGuid>cb4398d6-b7f1-449a-ae02-828769679232</ProjectGuid>
<RootNamespace>WebApplicationBasic</RootNamespace> <RootNamespace>WebApplicationBasic</RootNamespace>
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">..\..\..\JavaScriptServices.sln\artifacts\obj\$(MSBuildProjectName)</BaseIntermediateOutputPath> <BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">.\obj</BaseIntermediateOutputPath>
<OutputPath Condition="'$(OutputPath)'=='' ">.\bin\</OutputPath> <OutputPath Condition="'$(OutputPath)'=='' ">.\bin\</OutputPath>
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
<SchemaVersion>2.0</SchemaVersion> <SchemaVersion>2.0</SchemaVersion>
<DevelopmentServerPort>2018</DevelopmentServerPort>
</PropertyGroup> </PropertyGroup>
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.targets" Condition="'$(VSToolsPath)' != ''" /> <Import Project="$(VSToolsPath)\DotNet.Web\Microsoft.DotNet.Web.targets" Condition="'$(VSToolsPath)' != ''" />
</Project> </Project>

View File

@@ -1,17 +1,17 @@
{ {
"dependencies": { "dependencies": {
"Microsoft.NETCore.App": { "Microsoft.NETCore.App": {
"version": "1.0.0", "version": "1.0.1",
"type": "platform" "type": "platform"
}, },
"Microsoft.AspNetCore.Diagnostics": "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": { "Microsoft.AspNetCore.Razor.Tools": {
"version": "1.0.0-preview2-final", "version": "1.0.0-preview2-final",
"type": "build" "type": "build"
}, },
"Microsoft.AspNetCore.Server.IISIntegration": "1.0.0", "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.AspNetCore.StaticFiles": "1.0.0",
"Microsoft.Extensions.Configuration.EnvironmentVariables": "1.0.0", "Microsoft.Extensions.Configuration.EnvironmentVariables": "1.0.0",
"Microsoft.Extensions.Configuration.Json": "1.0.0", "Microsoft.Extensions.Configuration.Json": "1.0.0",

View File

@@ -184,9 +184,12 @@ ClientBin/
*.dbproj.schemaview *.dbproj.schemaview
*.pfx *.pfx
*.publishsettings *.publishsettings
node_modules/
orleans.codegen.cs orleans.codegen.cs
# Workaround for https://github.com/aspnet/JavaScriptServices/issues/235
/node_modules/**
!/node_modules/_placeholder.txt
# RIA/Silverlight projects # RIA/Silverlight projects
Generated_Code/ Generated_Code/

View File

@@ -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.

View File

@@ -6,6 +6,7 @@
"skipDefaultLibCheck": true "skipDefaultLibCheck": true
}, },
"exclude": [ "exclude": [
"bin",
"node_modules" "node_modules"
] ]
} }

3
templates/package-builder/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
/node_modules/
/built/
/dist/

View File

@@ -1,7 +1,7 @@
{ {
"name": "generator-aspnetcore-spa-generator", "name": "generator-aspnetcore-spa-generator",
"version": "1.0.0", "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", "main": "index.js",
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1", "test": "echo \"Error: no test specified\" && exit 1",

View File

@@ -0,0 +1,180 @@
import * as glob from 'glob';
import * as gitignore from 'gitignore-parser';
import * as fs from 'fs';
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 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, 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' }
};
function isTextFile(filename: string): boolean {
return textFileExtensions.indexOf(path.extname(filename).toLowerCase()) >= 0;
}
function writeFileEnsuringDirExists(root: string, filename: string, contents: string | Buffer) {
let fullPath = path.join(root, filename);
mkdirp.sync(path.dirname(fullPath));
fs.writeFileSync(fullPath, contents);
}
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).
let gitIgnorePath = path.join(root, 'template_gitignore');
let gitignoreEvaluator = fs.existsSync(gitIgnorePath)
? gitignore.compile(fs.readFileSync(gitIgnorePath, 'utf8'))
: { accepts: () => true };
return glob.sync('**/*', { cwd: root, dot: true, nodir: true })
.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 }[], forceInclusion: RegExp) {
listFilesExcludingGitignored(sourceRoot, forceInclusion).forEach(fn => {
let sourceContent = fs.readFileSync(path.join(sourceRoot, fn));
// For text files, replace hardcoded values with template tags
if (isTextFile(fn)) {
let sourceText = sourceContent.toString('utf8');
contentReplacements.forEach(replacement => {
sourceText = sourceText.replace(replacement.from, replacement.to);
});
sourceContent = new Buffer(sourceText, 'utf8');
}
// Also apply replacements in filenames
filenameReplacements.forEach(replacement => {
fn = fn.replace(replacement.from, replacement.to);
});
writeFileEnsuringDirExists(destRoot, fn, sourceContent);
});
}
function copyRecursive(sourceRoot: string, destRoot: string, matchGlob: string) {
glob.sync(matchGlob, { cwd: sourceRoot, dot: true, nodir: true })
.forEach(fn => {
const sourceContent = fs.readFileSync(path.join(sourceRoot, fn));
writeFileEnsuringDirExists(destRoot, fn, sourceContent);
});
}
function buildYeomanNpmPackage() {
const outputRoot = './dist/generator-aspnetcore-spa';
const outputTemplatesRoot = path.join(outputRoot, 'app/templates');
rimraf.sync(outputTemplatesRoot);
// Copy template files
const filenameReplacements = [
{ from: /.*\.xproj$/, to: 'tokenreplace-namePascalCase.xproj' }
];
const contentReplacements = [
{ from: /\bWebApplicationBasic\b/g, to: '<%= namePascalCase %>' },
{ from: /<ProjectGuid>[0-9a-f\-]{36}<\/ProjectGuid>/g, to: '<ProjectGuid><%= projectGuid %></ProjectGuid>' },
{ from: /<RootNamespace>.*?<\/RootNamespace>/g, to: '<RootNamespace><%= namePascalCase %></RootNamespace>'},
{ from: /\s*<BaseIntermediateOutputPath.*?<\/BaseIntermediateOutputPath>/g, to: '' },
{ from: /\s*<OutputPath.*?<\/OutputPath>/g, to: '' },
];
_.forEach(templates, (templateConfig, templateName) => {
const outputDir = path.join(outputTemplatesRoot, templateName);
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)
const tempRoot = './tmp';
copyRecursive(path.join(tempRoot, 'yeoman'), outputRoot, '**/*.js');
copyRecursive(yeomanGeneratorSource, outputRoot, '**/!(*.ts)');
// 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' },
// 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: /<ProjectGuid>[0-9a-f\-]{36}<\/ProjectGuid>/g, to: `<ProjectGuid>${projectGuid}</ProjectGuid>` },
{ from: /<RootNamespace>.*?<\/RootNamespace>/g, to: `<RootNamespace>${sourceProjectName}</RootNamespace>`},
{ from: /\s*<BaseIntermediateOutputPath.*?<\/BaseIntermediateOutputPath>/g, to: '' },
{ from: /\s*<OutputPath.*?<\/OutputPath>/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, templateConfig.forceInclusion);
// 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 },
], [], null);
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');
}
// 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();

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2012/06/nuspec.xsd">
<metadata>
<id>Microsoft.AspNetCore.Spa.Templates</id>
<version>{version}</version>
<title>Class Library and Console Application Templates for .NET Core</title>
<authors>Microsoft</authors>
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<description>My package description.</description>
<dependencies>
<group targetFramework=".NETCoreApp,Version=v1.0">
<dependency id="Microsoft.TemplateEngine.Orchestrator.RunnableProjects" version="1.0.0" />
</group>
</dependencies>
</metadata>
<files>
<file src="templates/**" />
</files>
</package>

View File

@@ -5,6 +5,9 @@ import * as glob from 'glob';
const yosay = require('yosay'); const yosay = require('yosay');
const toPascalCase = require('to-pascal-case'); 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 = [ const templates = [
{ value: 'angular-2', name: 'Angular 2' }, { value: 'angular-2', name: 'Angular 2' },
{ value: 'knockout', name: 'Knockout' }, { value: 'knockout', name: 'Knockout' },
@@ -14,16 +17,19 @@ const templates = [
class MyGenerator extends yeoman.Base { class MyGenerator extends yeoman.Base {
private _answers: any; private _answers: any;
private _optionOrPrompt: YeomanPrompt;
constructor(args: string | string[], options: any) { constructor(args: string | string[], options: any) {
super(args, options); super(args, options);
this._optionOrPrompt = optionOrPrompt;
this.log(yosay('Welcome to the ASP.NET Core Single-Page App generator!')); this.log(yosay('Welcome to the ASP.NET Core Single-Page App generator!'));
} }
prompting() { prompting() {
const done = this.async(); const done = this.async();
this.prompt([{ this.option('projectguid');
this._optionOrPrompt([{
type: 'list', type: 'list',
name: 'framework', name: 'framework',
message: 'Framework', message: 'Framework',
@@ -36,7 +42,7 @@ class MyGenerator extends yeoman.Base {
}], answers => { }], answers => {
this._answers = answers; this._answers = answers;
this._answers.namePascalCase = toPascalCase(answers.name); this._answers.namePascalCase = toPascalCase(answers.name);
this._answers.projectGuid = uuid.v4(); this._answers.projectGuid = this.options['projectguid'] || uuid.v4();
done(); done();
}); });
} }
@@ -52,6 +58,18 @@ class MyGenerator extends yeoman.Base {
outputFn = path.join(path.dirname(fn), '.gitignore'); 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( this.fs.copyTpl(
path.join(templateRoot, fn), path.join(templateRoot, fn),
this.destinationPath(outputFn), this.destinationPath(outputFn),

View File

@@ -1,6 +1,6 @@
{ {
"name": "generator-aspnetcore-spa", "name": "generator-aspnetcore-spa",
"version": "0.2.3", "version": "0.2.9",
"description": "Single-Page App templates for ASP.NET Core", "description": "Single-Page App templates for ASP.NET Core",
"author": "Microsoft", "author": "Microsoft",
"license": "Apache-2.0", "license": "Apache-2.0",
@@ -15,6 +15,7 @@
"node-uuid": "^1.4.7", "node-uuid": "^1.4.7",
"to-pascal-case": "^1.0.0", "to-pascal-case": "^1.0.0",
"yeoman-generator": "^0.20.2", "yeoman-generator": "^0.20.2",
"yeoman-option-or-prompt": "^1.0.2",
"yosay": "^1.1.1" "yosay": "^1.1.1"
} }
} }

View File

@@ -7,6 +7,6 @@
}, },
"exclude": [ "exclude": [
"node_modules", "node_modules",
"generator-aspnetcore-spa" "dist"
] ]
} }

Some files were not shown because too many files have changed in this diff Show More