Move packages under top-level 'src' folder

This commit is contained in:
SteveSandersonMS
2016-01-26 15:06:36 +00:00
parent 5e5b12dc54
commit 47c956cc4d
43 changed files with 5 additions and 5 deletions

View File

@@ -0,0 +1 @@
/bin/

View File

@@ -0,0 +1,54 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Http.Extensions;
using Microsoft.AspNet.NodeServices;
using Microsoft.AspNet.Razor.TagHelpers;
using Microsoft.Extensions.PlatformAbstractions;
namespace Microsoft.AspNet.AngularServices
{
[HtmlTargetElement(Attributes = PrerenderModuleAttributeName)]
public class AngularPrerenderTagHelper : TagHelper
{
static INodeServices fallbackNodeServices; // Used only if no INodeServices was registered with DI
const string PrerenderModuleAttributeName = "asp-ng2-prerender-module";
const string PrerenderExportAttributeName = "asp-ng2-prerender-export";
[HtmlAttributeName(PrerenderModuleAttributeName)]
public string ModuleName { get; set; }
[HtmlAttributeName(PrerenderExportAttributeName)]
public string ExportName { get; set; }
private IHttpContextAccessor contextAccessor;
private INodeServices nodeServices;
public AngularPrerenderTagHelper(IServiceProvider serviceProvider, IHttpContextAccessor contextAccessor)
{
this.contextAccessor = contextAccessor;
this.nodeServices = (INodeServices)serviceProvider.GetService(typeof (INodeServices)) ?? fallbackNodeServices;
// Consider removing the following. Having it means you can get away with not putting app.AddNodeServices()
// in your startup file, but then again it might be confusing that you don't need to.
if (this.nodeServices == null) {
var appEnv = (IApplicationEnvironment)serviceProvider.GetService(typeof (IApplicationEnvironment));
this.nodeServices = fallbackNodeServices = Configuration.CreateNodeServices(NodeHostingModel.Http, appEnv.ApplicationBasePath);
}
}
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
var result = await AngularRenderer.RenderToString(
nodeServices: this.nodeServices,
componentModuleName: this.ModuleName,
componentExportName: this.ExportName,
componentTagName: output.TagName,
requestUrl: UriHelper.GetEncodedUrl(this.contextAccessor.HttpContext.Request)
);
output.SuppressOutput();
output.PostElement.AppendHtml(result);
}
}
}

View File

@@ -0,0 +1,25 @@
using System.Threading.Tasks;
using Microsoft.AspNet.NodeServices;
namespace Microsoft.AspNet.AngularServices
{
public static class AngularRenderer
{
private static StringAsTempFile nodeScript;
static AngularRenderer() {
// Consider populating this lazily
var script = EmbeddedResourceReader.Read(typeof (AngularRenderer), "/Content/Node/angular-rendering.js");
nodeScript = new StringAsTempFile(script); // Will be cleaned up on process exit
}
public static async Task<string> RenderToString(INodeServices nodeServices, string componentModuleName, string componentExportName, string componentTagName, string requestUrl) {
return await nodeServices.InvokeExport<string>(nodeScript.FileName, "renderToString", new {
moduleName = componentModuleName,
exportName = componentExportName,
tagName = componentTagName,
requestUrl = requestUrl
});
}
}
}

View File

@@ -0,0 +1,53 @@
var path = require('path');
var ngUniversal = require('angular2-universal-preview');
var ngUniversalRender = require('angular2-universal-preview/dist/server/src/render');
var ngCore = require('angular2/core');
var ngRouter = require('angular2/router');
function getExportOrThrow(moduleInstance, moduleFilename, exportName) {
if (!(exportName in moduleInstance)) {
throw new Error('The module "' + moduleFilename + '" has no export named "' + exportName + '"');
}
return moduleInstance[exportName];
}
function findAngularComponent(options) {
var resolvedPath = path.resolve(process.cwd(), options.moduleName);
var loadedModule = require(resolvedPath);
if (options.exportName) {
// If exportName is specified explicitly, use it
return getExportOrThrow(loadedModule, resolvedPath, options.exportName);
} else if (typeof loadedModule === 'function') {
// Otherwise, if the module itself is a function, assume that is the component
return loadedModule;
} else if (typeof loadedModule.default === 'function') {
// Otherwise, if the module has a default export which is a function, assume that is the component
return loadedModule.default;
} else {
// Otherwise, guess the export name by converting tag-name to PascalCase
var tagNameAsPossibleExport = options.tagName.replace(/(-|^)([a-z])/g, function (m1, m2, char) { return char.toUpperCase(); });
return getExportOrThrow(loadedModule, resolvedPath, tagNameAsPossibleExport);
}
}
module.exports = {
renderToString: function(callback, options) {
try {
var component = findAngularComponent(options);
var serverBindings = [
ngRouter.ROUTER_BINDINGS,
ngUniversal.HTTP_PROVIDERS,
ngCore.provide(ngUniversal.BASE_URL, { useValue: options.requestUrl }),
ngCore.provide(ngRouter.APP_BASE_HREF, { useValue: '/' }),
ngUniversal.SERVER_LOCATION_PROVIDERS
];
return ngUniversalRender.renderToString(component, serverBindings).then(
function(successValue) { callback(null, successValue); },
function(errorValue) { callback(errorValue); }
);
} catch (synchronousException) {
callback(synchronousException);
}
}
};

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.Props" Condition="'$(VSToolsPath)' != ''" />
<PropertyGroup Label="Globals">
<ProjectGuid>421807e6-b62c-417b-b901-46c5dedaa8f1</ProjectGuid>
<RootNamespace>Microsoft.AspNet.AngularServices</RootNamespace>
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">..\artifacts\obj\$(MSBuildProjectName)</BaseIntermediateOutputPath>
<OutputPath Condition="'$(OutputPath)'=='' ">..\artifacts\bin\$(MSBuildProjectName)\</OutputPath>
</PropertyGroup>
<PropertyGroup>
<SchemaVersion>2.0</SchemaVersion>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.targets" Condition="'$(VSToolsPath)' != ''" />
</Project>

View File

@@ -0,0 +1,44 @@
using System;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.AspNet.Mvc.Rendering;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
namespace Microsoft.AspNet.AngularServices {
public static class PrimeCacheHelper {
public static async Task<HtmlString> PrimeCache(this IHtmlHelper html, string url) {
// TODO: Consider deduplicating the PrimeCache calls (that is, if there are multiple requests to precache
// the same URL, only return nonempty for one of them). This will make it easier to auto-prime-cache any
// HTTP requests made during server-side rendering, without risking unnecessary duplicate requests.
if (string.IsNullOrEmpty(url)) {
throw new ArgumentException("Value cannot be null or empty", "url");
}
try {
var request = html.ViewContext.HttpContext.Request;
var baseUri = new Uri(string.Concat(request.Scheme, "://", request.Host.ToUriComponent(), request.PathBase.ToUriComponent(), request.Path.ToUriComponent(), request.QueryString.ToUriComponent()));
var fullUri = new Uri(baseUri, url);
var response = await new HttpClient().GetAsync(fullUri.ToString());
var responseBody = await response.Content.ReadAsStringAsync();
return new HtmlString(FormatAsScript(url, response.StatusCode, responseBody));
} catch (Exception ex) {
var logger = (ILogger)html.ViewContext.HttpContext.ApplicationServices.GetService(typeof (ILogger));
if (logger != null) {
logger.LogWarning("Error priming cache for URL: " + url, ex);
}
return new HtmlString(string.Empty);
}
}
private static string FormatAsScript(string url, HttpStatusCode responseStatusCode, string responseBody)
{
return string.Format(@"<script>window.__preCachedResponses = window.__preCachedResponses || {{}}; window.__preCachedResponses[{0}] = {1};</script>",
JsonConvert.SerializeObject(url),
JsonConvert.SerializeObject(new { statusCode = responseStatusCode, body = responseBody })
);
}
}
}

View File

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

View File

@@ -0,0 +1,3 @@
/src/
/tsconfig.json
/build.js

View File

@@ -0,0 +1,33 @@
// -------------
// No need to invoke this directly. To run a build, execute:
// npm run prepublish
// -------------
var Builder = require('systemjs-builder');
var builder = new Builder('./');
builder.config({
defaultJSExtensions: true,
paths: {
'angular2-aspnet': 'dist/Exports',
'angular2-aspnet/*': 'dist/*'
},
meta: {
'angular2/*': { build: false },
'rxjs/*': { build: false }
}
});
var entryPoint = 'dist/Exports';
var tasks = [
builder.bundle(entryPoint, './bundles/angular2-aspnet.js'),
builder.bundle(entryPoint, './bundles/angular2-aspnet.min.js', { minify: true })
];
Promise.all(tasks)
.then(function() {
console.log('Build complete');
})
.catch(function(err) {
console.error('Build error');
console.error(err);
});

View File

@@ -0,0 +1,25 @@
{
"name": "angular2-aspnet",
"version": "0.0.3",
"description": "Helpers for Angular 2 apps built on ASP.NET",
"main": "./dist/Exports",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"prepublish": "tsc && node build.js"
},
"repository": {
"type": "git",
"url": "https://github.com/aspnet/NodeServices/tree/master/Microsoft.AspNet.AngularServices"
},
"typings": "dist/Exports",
"author": "Microsoft",
"license": "Apache-2.0",
"peerDependencies": {
"angular2": "2.0.0-beta.1",
"rxjs": "5.0.0-beta.0"
},
"devDependencies": {
"systemjs-builder": "^0.14.11",
"typescript": "^1.7.3"
}
}

View File

@@ -0,0 +1,59 @@
If you just want to use this package, then you *don't have to build it*. Instead, just grab the prebuilt package from NPM:
npm install angular2-aspnet
The rest of this file is notes for anyone contributing to this package itself.
## How to build
Run the following:
npm install
npm run prepublish
Requirements:
* Node, NPM
* `tsc` installed globally (via `npm install -g typescript`)
## Project structure
This package is intended to be consumable both on the server in Node.js, and on the client. Also, it's written in TypeScript,
which neither of those environments knows natively, but the TypeScript type definitions need to get delivered with the package
so that developers get a good IDE experience when consuming it.
The build process is therefore:
1. Compile the TypeScript to produce the development-time (.d.ts) and server-side (.js) artifacts
`tsc` reads `tsconfig.json` and is instructed to compile all the `.ts` files in `src/`. It produces a corresponding
structure of `.js` and `.d.ts` files in `dist/`.
When a developer consumes the resulting package (via `npm install angular2-aspnet`),
- No additional copy of `angular2` will be installed, because this package's dependency on it is declared as a
`peerDependency`. This means it will work with whatever (compatible) version of `angular2` is already installed.
- At runtime inside Node.js, the `main` configuration in `package.json` means the developer can use a standard
`import` statement to consume this package (i.e., `import * from 'angular2-aspnet';` in either JS or TS files).
- At development time inside an IDE such as Visual Studio Code, the `typings` configuration in `package.json` means
the IDE will use the corresponding `.d.ts` file as type metadata for the variable imported that way.
2. Use the SystemJS builder to produce the client-side artifacts
`build.js` uses the SystemJS Builder API to combine files in `dist/` into `.js` files ready for use in client-side
SystemJS environments, and puts them in `bundles/`. The bundle files contain `System.register` calls so that any
other part of your client-side code that tries to import `angular2-aspnet` via SystemJS will get that module at runtime.
To make it work in an application:
- Set up some build step that copies your chosen bundle file from `bundles/` to some location where it will
be served to the client
- Below your `<script>` tag that loads SystemJS itself, and above the `<script>` tag that makes the first call to
`System.import`, have a `<script>` tag that loads the desired `angular2-aspnet.js` bundle file
For an example, see https://github.com/aspnet/NodeServices/tree/master/samples/angular/MusicStore
Of course, you can also bundle the `angular2-aspnet.js` file into a larger SystemJS bundle if you want to combine
it with the rest of the code in your application.
Currently, this build system does *not* attempt to send sourcemaps of the original TypeScript to the client. This
could be added if a strong need emerges.

View File

@@ -0,0 +1,59 @@
import { provide, Injectable, Provider } from 'angular2/core';
import { Connection, ConnectionBackend, Http, XHRBackend, RequestOptions, Request, RequestMethod, Response, ResponseOptions, ReadyState } from 'angular2/http';
@Injectable()
export class CachePrimedConnectionBackend extends ConnectionBackend {
private _preCachedResponses: PreCachedResponses;
constructor(private _underlyingBackend: ConnectionBackend, private _baseResponseOptions: ResponseOptions) {
super();
this._preCachedResponses = (<any>window).__preCachedResponses || {};
}
public createConnection(request: Request): Connection {
let cacheKey = request.url;
if (request.method === RequestMethod.Get && this._preCachedResponses.hasOwnProperty(cacheKey)) {
return new CacheHitConnection(request, this._preCachedResponses[cacheKey], this._baseResponseOptions);
} else {
return this._underlyingBackend.createConnection(request);
}
}
}
class CacheHitConnection implements Connection {
readyState: ReadyState;
request: Request;
response: any;
constructor (req: Request, cachedResponse: PreCachedResponse, baseResponseOptions: ResponseOptions) {
this.request = req;
this.readyState = ReadyState.Done;
// Workaround for difficulty consuming CommonJS default exports in TypeScript. Note that it has to be a dynamic
// 'require', and not an 'import' statement, because the module isn't available on the server.
let obsCtor: any = require('rxjs/Observable').Observable;
this.response = new obsCtor(responseObserver => {
let response = new Response(new ResponseOptions({ body: cachedResponse.body, status: cachedResponse.statusCode }));
responseObserver.next(response);
responseObserver.complete();
});
}
}
declare var require: any; // Part of the workaround mentioned below. Can remove this after updating Angular.
interface PreCachedResponses {
[url: string]: PreCachedResponse;
}
interface PreCachedResponse {
statusCode: number;
body: string;
}
export const CACHE_PRIMED_HTTP_PROVIDERS = [
provide(Http, {
useFactory: (xhrBackend, requestOptions, responseOptions) => new Http(new CachePrimedConnectionBackend(xhrBackend, responseOptions), requestOptions),
deps: [XHRBackend, RequestOptions, ResponseOptions]
}),
];

View File

@@ -0,0 +1,2 @@
export * from './CachePrimedHttp';
export * from './Validation';

View File

@@ -0,0 +1,34 @@
import { ControlGroup } from 'angular2/common';
import { Response } from 'angular2/http';
export class Validation {
public static showValidationErrors(response: ValidationErrorResult | Response, controlGroup: ControlGroup): void {
if (response instanceof Response) {
var httpResponse = <Response>response;
response = <ValidationErrorResult>(httpResponse.json());
}
// It's not yet clear whether this is a legitimate and supported use of the ng.ControlGroup API.
// Need feedback from the Angular 2 team on whether there's a better way.
var errors = <ValidationErrorResult>response;
Object.keys(errors || {}).forEach(key => {
errors[key].forEach(errorMessage => {
// If there's a specific control for this key, then use it. Otherwise associate the error
// with the whole control group.
var control = controlGroup.controls[key] || controlGroup;
// This is rough. Need to find out if there's a better way, or if this is even supported.
if (!control.errors) {
(<any>control)._errors = {};
}
control.errors[errorMessage] = true;
});
});
}
}
export interface ValidationErrorResult {
[propertyName: string]: string[];
}

View File

@@ -0,0 +1,14 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"sourceMap": false,
"declaration": true,
"experimentalDecorators": true,
"noLib": false,
"outDir": "./dist"
},
"exclude": [
"node_modules"
]
}

View File

@@ -0,0 +1,33 @@
{
"version": "1.0.0-alpha7",
"description": "Helpers for building Angular 2 applications on ASP.NET 5.",
"authors": [ "Microsoft" ],
"repository": {
"type": "git",
"url": "git://github.com/aspnet/nodeservices"
},
"tooling": {
"defaultNamespace": "Microsoft.AspNet.AngularServices"
},
"frameworks": {
"net451": { },
"dotnet5.4": {
"dependencies": {
"Microsoft.CSharp": "4.0.1-beta-*",
"System.Collections": "4.0.11-beta-*",
"System.Linq": "4.0.1-beta-*",
"System.Net.Http": "4.0.1-beta-*",
"System.Runtime": "4.0.21-beta-*",
"System.Threading": "4.0.11-beta-*"
}
}
},
"dependencies": {
"Microsoft.AspNet.Mvc.TagHelpers": "6.0.0-rc1-final",
"Microsoft.AspNet.NodeServices": "1.0.0-alpha7",
"Microsoft.AspNet.SpaServices": "1.0.0-alpha7-1"
},
"resource": [
"Content/**/*"
]
}

View File

@@ -0,0 +1 @@
/bin/

View File

@@ -0,0 +1,26 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.PlatformAbstractions;
namespace Microsoft.AspNet.NodeServices {
public static class Configuration {
public static void AddNodeServices(this IServiceCollection serviceCollection, NodeHostingModel hostingModel = NodeHostingModel.Http) {
serviceCollection.AddSingleton(typeof(INodeServices), (serviceProvider) => {
var appEnv = serviceProvider.GetRequiredService<IApplicationEnvironment>();
return CreateNodeServices(hostingModel, appEnv.ApplicationBasePath);
});
}
public static INodeServices CreateNodeServices(NodeHostingModel hostingModel, string projectPath)
{
switch (hostingModel)
{
case NodeHostingModel.Http:
return new HttpNodeInstance(projectPath);
case NodeHostingModel.InputOutputStream:
return new InputOutputStreamNodeInstance(projectPath);
default:
throw new System.ArgumentException("Unknown hosting model: " + hostingModel.ToString());
}
}
}
}

View File

@@ -0,0 +1,77 @@
// 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.
var http = require('http');
var path = require('path');
var requestedPortOrZero = parseInt(process.argv[2]) || 0; // 0 means 'let the OS decide'
autoQuitOnFileChange(process.cwd(), ['.js', '.json', '.html']);
var server = http.createServer(function(req, res) {
readRequestBodyAsJson(req, function(bodyJson) {
var resolvedPath = path.resolve(process.cwd(), bodyJson.moduleName);
var invokedModule = require(resolvedPath);
var func = bodyJson.exportedFunctionName ? invokedModule[bodyJson.exportedFunctionName] : invokedModule;
if (!func) {
throw new Error('The module "' + resolvedPath + '" has no export named "' + bodyJson.exportedFunctionName + '"');
}
var hasSentResult = false;
var callback = function(errorValue, successValue) {
if (!hasSentResult) {
hasSentResult = true;
if (errorValue) {
res.statusCode = 500;
if (errorValue.stack) {
res.end(errorValue.stack);
} else {
res.end(errorValue.toString());
}
} else if (typeof successValue !== 'string') {
// Arbitrary object/number/etc - JSON-serialize it
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify(successValue));
} else {
// String - can bypass JSON-serialization altogether
res.setHeader('Content-Type', 'text/plain');
res.end(successValue);
}
}
};
try {
func.apply(null, [callback].concat(bodyJson.args));
} catch (synchronousException) {
callback(synchronousException, null);
}
});
});
server.listen(requestedPortOrZero, 'localhost', function () {
// Signal to HttpNodeHost which port it should make its HTTP connections on
console.log('[Microsoft.AspNet.NodeServices.HttpNodeHost:Listening on port ' + server.address().port + '\]');
// Signal to the NodeServices base class that we're ready to accept invocations
console.log('[Microsoft.AspNet.NodeServices:Listening]');
});
function readRequestBodyAsJson(request, callback) {
var requestBodyAsString = '';
request
.on('data', function(chunk) { requestBodyAsString += chunk; })
.on('end', function() { callback(JSON.parse(requestBodyAsString)); });
}
function autoQuitOnFileChange(rootDir, extensions) {
// Note: This will only work on Windows/OS X, because the 'recursive' option isn't supported on Linux.
// Consider using a different watch mechanism (though ideally without forcing further NPM dependencies).
var fs = require('fs');
var path = require('path');
fs.watch(rootDir, { persistent: false, recursive: true }, function(event, filename) {
var ext = path.extname(filename);
if (extensions.indexOf(ext) >= 0) {
console.log('Restarting due to file change: ' + filename);
process.exit(0);
}
});
}

View File

@@ -0,0 +1,23 @@
var path = require('path');
var readline = require('readline');
var invocationPrefix = 'invoke:';
function invocationCallback(errorValue, successValue) {
if (errorValue) {
throw new Error('InputOutputStreamHost doesn\'t support errors. Got error: ' + errorValue.toString());
} else {
var serializedResult = JSON.stringify(successValue);
console.log(serializedResult);
}
}
readline.createInterface({ input: process.stdin }).on('line', function (message) {
if (message && message.substring(0, invocationPrefix.length) === invocationPrefix) {
var invocation = JSON.parse(message.substring(invocationPrefix.length));
var invokedModule = require(path.resolve(process.cwd(), invocation.moduleName));
var func = invocation.exportedFunctionName ? invokedModule[invocation.exportedFunctionName] : invokedModule;
func.apply(null, [invocationCallback].concat(invocation.args));
}
});
console.log('[Microsoft.AspNet.NodeServices:Listening]'); // The .NET app waits for this signal before sending any invocations

View File

@@ -0,0 +1,64 @@
using System;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
namespace Microsoft.AspNet.NodeServices {
internal class HttpNodeInstance : OutOfProcessNodeInstance {
private readonly static Regex PortMessageRegex = new Regex(@"^\[Microsoft.AspNet.NodeServices.HttpNodeHost:Listening on port (\d+)\]$");
private readonly static JsonSerializerSettings jsonSerializerSettings = new JsonSerializerSettings {
ContractResolver = new CamelCasePropertyNamesContractResolver()
};
private int _portNumber;
public HttpNodeInstance(string projectPath, int port = 0)
: base(EmbeddedResourceReader.Read(typeof(HttpNodeInstance), "/Content/Node/entrypoint-http.js"), projectPath, port.ToString())
{
}
public override async Task<T> Invoke<T>(NodeInvocationInfo invocationInfo) {
await this.EnsureReady();
using (var client = new HttpClient()) {
// TODO: Use System.Net.Http.Formatting (PostAsJsonAsync etc.)
var payloadJson = JsonConvert.SerializeObject(invocationInfo, jsonSerializerSettings);
var payload = new StringContent(payloadJson, Encoding.UTF8, "application/json");
var response = await client.PostAsync("http://localhost:" + this._portNumber, payload);
var responseString = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode) {
throw new Exception("Call to Node module failed with error: " + responseString);
}
var responseIsJson = response.Content.Headers.ContentType.MediaType == "application/json";
if (responseIsJson) {
return JsonConvert.DeserializeObject<T>(responseString);
} else if (typeof(T) != typeof(string)) {
throw new System.ArgumentException("Node module responded with non-JSON string. This cannot be converted to the requested generic type: " + typeof(T).FullName);
} else {
return (T)(object)responseString;
}
}
}
protected override void OnOutputDataReceived(string outputData) {
var match = this._portNumber != 0 ? null : PortMessageRegex.Match(outputData);
if (match != null && match.Success) {
this._portNumber = int.Parse(match.Groups[1].Captures[0].Value);
} else {
base.OnOutputDataReceived(outputData);
}
}
protected override void OnBeforeLaunchProcess() {
// Prepare to receive a new port number
this._portNumber = 0;
}
}
}

View File

@@ -0,0 +1,58 @@
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
namespace Microsoft.AspNet.NodeServices {
// This is just to demonstrate that other transports are possible. This implementation is extremely
// dubious - if the Node-side code fails to conform to the expected protocol in any way (e.g., has an
// error), then it will just hang forever. So don't use this.
//
// But it's fast - the communication round-trip time is about 0.2ms (tested on OS X on a recent machine),
// versus 2-3ms for the HTTP transport.
//
// Instead of directly using stdin/stdout, we could use either regular sockets (TCP) or use named pipes
// on Windows and domain sockets on Linux / OS X, but either way would need a system for framing the
// requests, associating them with responses, and scheduling use of the comms channel.
internal class InputOutputStreamNodeInstance : OutOfProcessNodeInstance
{
private SemaphoreSlim _invocationSemaphore = new SemaphoreSlim(1);
private TaskCompletionSource<string> _currentInvocationResult;
private readonly static JsonSerializerSettings jsonSerializerSettings = new JsonSerializerSettings {
ContractResolver = new CamelCasePropertyNamesContractResolver()
};
public InputOutputStreamNodeInstance(string projectPath)
: base(EmbeddedResourceReader.Read(typeof(InputOutputStreamNodeInstance), "/Content/Node/entrypoint-stream.js"), projectPath)
{
}
public override async Task<T> Invoke<T>(NodeInvocationInfo invocationInfo) {
await this._invocationSemaphore.WaitAsync();
try {
await this.EnsureReady();
var payloadJson = JsonConvert.SerializeObject(invocationInfo, jsonSerializerSettings);
var nodeProcess = this.NodeProcess;
this._currentInvocationResult = new TaskCompletionSource<string>();
nodeProcess.StandardInput.Write("\ninvoke:");
nodeProcess.StandardInput.WriteLine(payloadJson); // WriteLineAsync isn't supported cross-platform
var resultString = await this._currentInvocationResult.Task;
return JsonConvert.DeserializeObject<T>(resultString);
} finally {
this._invocationSemaphore.Release();
this._currentInvocationResult = null;
}
}
protected override void OnOutputDataReceived(string outputData) {
if (this._currentInvocationResult != null) {
this._currentInvocationResult.SetResult(outputData);
} else {
base.OnOutputDataReceived(outputData);
}
}
}
}

View File

@@ -0,0 +1,8 @@
namespace Microsoft.AspNet.NodeServices {
public class NodeInvocationInfo
{
public string ModuleName;
public string ExportedFunctionName;
public object[] Args;
}
}

View File

@@ -0,0 +1,152 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
namespace Microsoft.AspNet.NodeServices {
/**
* Class responsible for launching the Node child process, determining when it is ready to accept invocations,
* and finally killing it when the parent process exits. Also it restarts the child process if it dies.
*/
public abstract class OutOfProcessNodeInstance : INodeServices {
private object _childProcessLauncherLock;
private bool disposed;
private StringAsTempFile _entryPointScript;
private string _projectPath;
private string _commandLineArguments;
private Process _nodeProcess;
private TaskCompletionSource<bool> _nodeProcessIsReadySource;
protected Process NodeProcess {
get {
// This is only exposed to support the unreliable OutOfProcessNodeRunner, which is just to verify that
// other hosting/transport mechanisms are possible. This shouldn't really be exposed.
return this._nodeProcess;
}
}
public OutOfProcessNodeInstance(string entryPointScript, string projectPath, string commandLineArguments = null)
{
this._childProcessLauncherLock = new object();
this._entryPointScript = new StringAsTempFile(entryPointScript);
this._projectPath = projectPath;
this._commandLineArguments = commandLineArguments ?? string.Empty;
}
public abstract Task<T> Invoke<T>(NodeInvocationInfo invocationInfo);
public Task<T> Invoke<T>(string moduleName, params object[] args) {
return this.InvokeExport<T>(moduleName, null, args);
}
public async Task<T> InvokeExport<T>(string moduleName, string exportedFunctionName, params object[] args) {
return await this.Invoke<T>(new NodeInvocationInfo {
ModuleName = moduleName,
ExportedFunctionName = exportedFunctionName,
Args = args
});
}
protected async Task EnsureReady() {
lock (this._childProcessLauncherLock) {
if (this._nodeProcess == null || this._nodeProcess.HasExited) {
var startInfo = new ProcessStartInfo("node") {
Arguments = "\"" + this._entryPointScript.FileName + "\" " + this._commandLineArguments,
UseShellExecute = false,
RedirectStandardInput = true,
RedirectStandardOutput = true,
RedirectStandardError = true,
WorkingDirectory = this._projectPath
};
// Append projectPath to NODE_PATH so it can locate node_modules
var existingNodePath = Environment.GetEnvironmentVariable("NODE_PATH") ?? string.Empty;
if (existingNodePath != string.Empty) {
existingNodePath += ":";
}
var nodePathValue = existingNodePath + Path.Combine(this._projectPath, "node_modules");
#if NET451
startInfo.EnvironmentVariables.Add("NODE_PATH", nodePathValue);
#else
startInfo.Environment.Add("NODE_PATH", nodePathValue);
#endif
this.OnBeforeLaunchProcess();
this._nodeProcess = Process.Start(startInfo);
this.ConnectToInputOutputStreams();
}
}
var task = this._nodeProcessIsReadySource.Task;
var initializationSucceeded = await task;
if (!initializationSucceeded) {
throw new InvalidOperationException("The Node.js process failed to initialize", task.Exception);
}
}
private void ConnectToInputOutputStreams() {
var initializationIsCompleted = false; // TODO: Make this thread-safe? (Interlocked.Exchange etc.)
this._nodeProcessIsReadySource = new TaskCompletionSource<bool>();
this._nodeProcess.OutputDataReceived += (sender, evt) => {
if (evt.Data == "[Microsoft.AspNet.NodeServices:Listening]" && !initializationIsCompleted) {
this._nodeProcessIsReadySource.SetResult(true);
initializationIsCompleted = true;
} else if (evt.Data != null) {
this.OnOutputDataReceived(evt.Data);
}
};
this._nodeProcess.ErrorDataReceived += (sender, evt) => {
if (evt.Data != null) {
this.OnErrorDataReceived(evt.Data);
if (!initializationIsCompleted) {
this._nodeProcessIsReadySource.SetResult(false);
initializationIsCompleted = true;
}
}
};
this._nodeProcess.BeginOutputReadLine();
this._nodeProcess.BeginErrorReadLine();
}
protected virtual void OnBeforeLaunchProcess() {
}
protected virtual void OnOutputDataReceived(string outputData) {
Console.WriteLine("[Node] " + outputData);
}
protected virtual void OnErrorDataReceived(string errorData) {
Console.WriteLine("[Node] " + errorData);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!disposed) {
if (disposing) {
this._entryPointScript.Dispose();
}
if (this._nodeProcess != null && !this._nodeProcess.HasExited) {
this._nodeProcess.Kill(); // TODO: Is there a more graceful way to end it? Or does this still let it perform any cleanup?
}
disposed = true;
}
}
~OutOfProcessNodeInstance() {
Dispose (false);
}
}
}

View File

@@ -0,0 +1,10 @@
using System;
using System.Threading.Tasks;
namespace Microsoft.AspNet.NodeServices {
public interface INodeServices : IDisposable {
Task<T> Invoke<T>(string moduleName, params object[] args);
Task<T> InvokeExport<T>(string moduleName, string exportedFunctionName, params object[] args);
}
}

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.Props" Condition="'$(VSToolsPath)' != ''" />
<PropertyGroup Label="Globals">
<ProjectGuid>b0fa4175-8b29-4904-9780-28b3c24b0567</ProjectGuid>
<RootNamespace>Microsoft.AspNet.NodeServices</RootNamespace>
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">..\NodeServices.sln\artifacts\obj\$(MSBuildProjectName)</BaseIntermediateOutputPath>
<OutputPath Condition="'$(OutputPath)'=='' ">..\NodeServices.sln\artifacts\bin\$(MSBuildProjectName)\</OutputPath>
</PropertyGroup>
<PropertyGroup>
<SchemaVersion>2.0</SchemaVersion>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.targets" Condition="'$(VSToolsPath)' != ''" />
</Project>

View File

@@ -0,0 +1,6 @@
namespace Microsoft.AspNet.NodeServices {
public enum NodeHostingModel {
Http,
InputOutputStream,
}
}

View File

@@ -0,0 +1,17 @@
using System;
using System.IO;
using System.Reflection;
namespace Microsoft.AspNet.NodeServices {
public static class EmbeddedResourceReader {
public static string Read(Type assemblyContainingType, string path) {
var asm = assemblyContainingType.GetTypeInfo().Assembly;
var embeddedResourceName = asm.GetName().Name + path.Replace("/", ".");
using (var stream = asm.GetManifestResourceStream(embeddedResourceName))
using (var sr = new StreamReader(stream)) {
return sr.ReadToEnd();
}
}
}
}

View File

@@ -0,0 +1,39 @@
using System;
using System.IO;
namespace Microsoft.AspNet.NodeServices {
// Makes it easier to pass script files to Node in a way that's sure to clean up after the process exits
public sealed class StringAsTempFile : IDisposable {
public string FileName { get; private set; }
private bool _disposedValue;
public StringAsTempFile(string content) {
this.FileName = Path.GetTempFileName();
File.WriteAllText(this.FileName, content);
}
private void DisposeImpl(bool disposing)
{
if (!_disposedValue) {
if (disposing) {
// TODO: dispose managed state (managed objects).
}
File.Delete(this.FileName);
_disposedValue = true;
}
}
public void Dispose()
{
DisposeImpl(true);
GC.SuppressFinalize(this);
}
~StringAsTempFile() {
DisposeImpl(false);
}
}
}

View File

@@ -0,0 +1,33 @@
{
"version": "1.0.0-alpha7",
"description": "Invoke Node.js modules at runtime in ASP.NET 5 applications.",
"authors": [ "Microsoft" ],
"repository": {
"type": "git",
"url": "git://github.com/aspnet/nodeservices"
},
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "1.0.0-rc1-final",
"Microsoft.Extensions.PlatformAbstractions": "1.0.0-rc1-final",
"Newtonsoft.Json": "8.0.1-beta3",
"System.Net.Http": "4.0.1-beta-*"
},
"frameworks": {
"net451": { },
"dotnet5.4": {
"dependencies": {
"Microsoft.CSharp": "4.0.1-beta-*",
"System.Collections": "4.0.11-beta-*",
"System.Console": "4.0.0-beta-*",
"System.Diagnostics.Process": "4.1.0-beta-*",
"System.IO.FileSystem": "4.0.1-beta-*",
"System.Linq": "4.0.1-beta-*",
"System.Text.RegularExpressions": "4.0.11-beta-*",
"System.Threading": "4.0.11-beta-*"
}
}
},
"resource": [
"Content/**/*"
]
}

View File

@@ -0,0 +1 @@
/bin/

View File

@@ -0,0 +1,58 @@
var fs = require('fs');
var path = require('path');
var React = require('react');
var ReactDOMServer = require('react-dom/server');
var createMemoryHistory = require('history/lib/createMemoryHistory');
var babelCore = require('babel-core');
var babelConfig = {};
var origJsLoader = require.extensions['.js'];
require.extensions['.js'] = loadViaBabel;
require.extensions['.jsx'] = loadViaBabel;
function findReactComponent(options) {
var resolvedPath = path.resolve(process.cwd(), options.moduleName);
var loadedModule = require(resolvedPath);
if (options.exportName) {
// If exportName is specified explicitly, use it
if (!(options.exportName in loadedModule)) {
throw new Error('The module "' + resolvedPath + '" has no export named "' + options.exportName + '"');
}
return loadedModule[options.exportName];
} else if (typeof loadedModule === 'function') {
// Otherwise, if the module itself is a function, assume that is the component
return loadedModule;
} else if (typeof loadedModule.default === 'function') {
// Otherwise, if the module has a default export which is a function, assume that is the component
return loadedModule.default;
} else {
throw new Error('Cannot find React component, because no export name was specified, and the module "' + resolvedPath + '" has no default exported class.');
}
}
function loadViaBabel(module, filename) {
// Assume that all the app's own code is ES2015+ (optionally with JSX), but that none of the node_modules are.
// The distinction is important because ES2015+ forces strict mode, and it may break ES3/5 if you try to run it in strict
// mode when the developer didn't expect that (e.g., current versions of underscore.js can't be loaded in strict mode).
var useBabel = filename.indexOf('node_modules') < 0;
if (useBabel) {
var transformedFile = babelCore.transformFileSync(filename, babelConfig);
return module._compile(transformedFile.code, filename);
} else {
return origJsLoader.apply(this, arguments);
}
}
module.exports = {
renderToString: function(callback, options) {
try {
var component = findReactComponent(options);
var history = createMemoryHistory(options.requestUrl);
var reactElement = React.createElement(component, { history: history });
var html = ReactDOMServer.renderToString(reactElement);
callback(null, html);
} catch (synchronousException) {
callback(synchronousException);
}
}
};

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.Props" Condition="'$(VSToolsPath)' != ''" />
<PropertyGroup Label="Globals">
<ProjectGuid>b04381de-991f-4831-a0b5-fe1bd3ef80c4</ProjectGuid>
<RootNamespace>Microsoft.AspNet.ReactServices</RootNamespace>
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">..\artifacts\obj\$(MSBuildProjectName)</BaseIntermediateOutputPath>
<OutputPath Condition="'$(OutputPath)'=='' ">..\artifacts\bin\$(MSBuildProjectName)\</OutputPath>
</PropertyGroup>
<PropertyGroup>
<SchemaVersion>2.0</SchemaVersion>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.targets" Condition="'$(VSToolsPath)' != ''" />
</Project>

View File

@@ -0,0 +1,51 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.NodeServices;
using Microsoft.AspNet.Razor.TagHelpers;
using Microsoft.Extensions.PlatformAbstractions;
namespace Microsoft.AspNet.ReactServices
{
[HtmlTargetElement(Attributes = PrerenderModuleAttributeName)]
public class ReactPrerenderTagHelper : TagHelper
{
static INodeServices fallbackNodeServices; // Used only if no INodeServices was registered with DI
const string PrerenderModuleAttributeName = "asp-react-prerender-module";
const string PrerenderExportAttributeName = "asp-react-prerender-export";
[HtmlAttributeName(PrerenderModuleAttributeName)]
public string ModuleName { get; set; }
[HtmlAttributeName(PrerenderExportAttributeName)]
public string ExportName { get; set; }
private IHttpContextAccessor contextAccessor;
private INodeServices nodeServices;
public ReactPrerenderTagHelper(IServiceProvider serviceProvider, IHttpContextAccessor contextAccessor)
{
this.contextAccessor = contextAccessor;
this.nodeServices = (INodeServices)serviceProvider.GetService(typeof (INodeServices)) ?? fallbackNodeServices;
// Consider removing the following. Having it means you can get away with not putting app.AddNodeServices()
// in your startup file, but then again it might be confusing that you don't need to.
if (this.nodeServices == null) {
var appEnv = (IApplicationEnvironment)serviceProvider.GetService(typeof(IApplicationEnvironment));
this.nodeServices = fallbackNodeServices = Configuration.CreateNodeServices(NodeHostingModel.Http, appEnv.ApplicationBasePath);
}
}
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
var request = this.contextAccessor.HttpContext.Request;
var result = await ReactRenderer.RenderToString(
nodeServices: this.nodeServices,
componentModuleName: this.ModuleName,
componentExportName: this.ExportName,
requestUrl: request.Path + request.QueryString.Value);
output.Content.SetHtmlContent(result);
}
}
}

View File

@@ -0,0 +1,24 @@
using System.Threading.Tasks;
using Microsoft.AspNet.NodeServices;
namespace Microsoft.AspNet.ReactServices
{
public static class ReactRenderer
{
private static StringAsTempFile nodeScript;
static ReactRenderer() {
// Consider populating this lazily
var script = EmbeddedResourceReader.Read(typeof (ReactRenderer), "/Content/Node/react-rendering.js");
nodeScript = new StringAsTempFile(script); // Will be cleaned up on process exit
}
public static async Task<string> RenderToString(INodeServices nodeServices, string componentModuleName, string componentExportName, string requestUrl) {
return await nodeServices.InvokeExport<string>(nodeScript.FileName, "renderToString", new {
moduleName = componentModuleName,
exportName = componentExportName,
requestUrl = requestUrl
});
}
}
}

View File

@@ -0,0 +1,32 @@
{
"version": "1.0.0-alpha7",
"description": "Helpers for building React applications on ASP.NET 5.",
"authors": [ "Microsoft" ],
"repository": {
"type": "git",
"url": "git://github.com/aspnet/nodeservices"
},
"tooling": {
"defaultNamespace": "Microsoft.AspNet.ReactServices"
},
"dependencies": {
"Microsoft.AspNet.Mvc.TagHelpers": "6.0.0-rc1-final",
"Microsoft.AspNet.NodeServices": "1.0.0-alpha7",
"Microsoft.AspNet.SpaServices": "1.0.0-alpha7-1"
},
"frameworks": {
"net451": { },
"dotnet5.4": {
"dependencies": {
"Microsoft.CSharp": "4.0.1-beta-*",
"System.Collections": "4.0.11-beta-*",
"System.Linq": "4.0.1-beta-*",
"System.Runtime": "4.0.21-beta-*",
"System.Threading": "4.0.11-beta-*"
}
}
},
"resource": [
"Content/**/*"
]
}

View File

@@ -0,0 +1 @@
/bin/

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0.23107" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0.23107</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.Props" Condition="'$(VSToolsPath)' != ''" />
<PropertyGroup Label="Globals">
<ProjectGuid>4624f728-6dff-44b6-93b5-3c7d9c94bf3f</ProjectGuid>
<RootNamespace>Microsoft.AspNet.SpaServices</RootNamespace>
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">..\artifacts\obj\$(MSBuildProjectName)</BaseIntermediateOutputPath>
<OutputPath Condition="'$(OutputPath)'=='' ">..\artifacts\bin\$(MSBuildProjectName)\</OutputPath>
</PropertyGroup>
<PropertyGroup>
<SchemaVersion>2.0</SchemaVersion>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.targets" Condition="'$(VSToolsPath)' != ''" />
</Project>

View File

@@ -0,0 +1,32 @@
using System;
using System.Collections.Generic;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Routing;
namespace Microsoft.AspNet.SpaServices
{
internal class SpaRouteConstraint : IRouteConstraint
{
private readonly string clientRouteTokenName;
public SpaRouteConstraint(string clientRouteTokenName) {
if (string.IsNullOrEmpty(clientRouteTokenName)) {
throw new ArgumentException("Value cannot be null or empty", "clientRouteTokenName");
}
this.clientRouteTokenName = clientRouteTokenName;
}
public bool Match(HttpContext httpContext, IRouter route, string routeKey, IDictionary<string, object> values, RouteDirection routeDirection)
{
var clientRouteValue = (values[this.clientRouteTokenName] as string) ?? string.Empty;
return !HasDotInLastSegment(clientRouteValue);
}
private bool HasDotInLastSegment(string uri)
{
var lastSegmentStartPos = uri.LastIndexOf('/');
return uri.IndexOf('.', lastSegmentStartPos + 1) >= 0;
}
}
}

View File

@@ -0,0 +1,53 @@
using System;
using System.Collections.Generic;
using Microsoft.AspNet.Routing;
using Microsoft.AspNet.SpaServices;
// Putting in this namespace so it's always available whenever MapRoute is
namespace Microsoft.AspNet.Builder
{
public static class SpaRouteExtensions
{
private const string ClientRouteTokenName = "clientRoute";
public static void MapSpaFallbackRoute(this IRouteBuilder routeBuilder, string name, object defaults, object constraints = null, object dataTokens = null)
{
MapSpaFallbackRoute(routeBuilder, name, /* templatePrefix */ (string)null, defaults, constraints, dataTokens);
}
public static void MapSpaFallbackRoute(this IRouteBuilder routeBuilder, string name, string templatePrefix, object defaults, object constraints = null, object dataTokens = null)
{
var template = CreateRouteTemplate(templatePrefix);
var constraintsDict = ObjectToDictionary(constraints);
constraintsDict.Add(ClientRouteTokenName, new SpaRouteConstraint(ClientRouteTokenName));
routeBuilder.MapRoute(name, template, defaults, constraintsDict, dataTokens);
}
private static string CreateRouteTemplate(string templatePrefix)
{
templatePrefix = templatePrefix ?? string.Empty;
if (templatePrefix.Contains("?")) {
// TODO: Consider supporting this. The {*clientRoute} part should be added immediately before the '?'
throw new ArgumentException("SPA fallback route templates don't support querystrings");
}
if (templatePrefix.Contains("#")) {
throw new ArgumentException("SPA fallback route templates should not include # characters. The hash part of a URI does not get sent to the server.");
}
if (templatePrefix != string.Empty && !templatePrefix.EndsWith("/")) {
templatePrefix += "/";
}
return templatePrefix + $"{{*{ ClientRouteTokenName }}}";
}
private static IDictionary<string, object> ObjectToDictionary(object value)
{
return value as IDictionary<string, object> ?? new RouteValueDictionary(value);
}
}
}

View File

@@ -0,0 +1,23 @@
{
"version": "1.0.0-alpha7-1",
"description": "Microsoft.AspNet.SpaServices",
"authors": [
"Microsoft"
],
"tags": [
""
],
"projectUrl": "",
"licenseUrl": "",
"dependencies": {
"Microsoft.AspNet.Mvc": "6.0.0-rc1-final",
"Microsoft.AspNet.Routing": "1.0.0-rc1-final"
},
"frameworks": {
"net451": { },
"dotnet5.4": {
"dependencies": {
}
}
}
}