Add aspnet-prerendering NPM package

This commit is contained in:
SteveSandersonMS
2016-03-10 23:22:29 +00:00
parent e5a6a05c97
commit ec9544c644
9 changed files with 220 additions and 0 deletions

View File

@@ -0,0 +1,4 @@
/typings/
/node_modules/
/*.js
/*.d.ts

View File

@@ -0,0 +1,3 @@
!/*.js
!/*.d.ts
/typings/

View File

@@ -0,0 +1,12 @@
Copyright (c) .NET Foundation. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
these files except in compliance with the License. You may obtain a copy of the
License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed
under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
CONDITIONS OF ANY KIND, either express or implied. See the License for the
specific language governing permissions and limitations under the License.

View File

@@ -0,0 +1,6 @@
# Not for general use
This NPM package is an internal implementation detail of the `Microsoft.AspNet.SpaServices` NuGet package.
You should not use this package directly in your own applications, because it is not supported, and there are no
guarantees about how its APIs will change in the future.

View File

@@ -0,0 +1,16 @@
{
"name": "aspnet-prerendering",
"version": "1.0.0",
"description": "Helpers for server-side rendering of JavaScript applications in ASP.NET projects. Works in conjunction with the Microsoft.AspNet.SpaServices NuGet package.",
"main": "index.js",
"scripts": {
"prepublish": "tsd update && tsc && echo 'Finished building NPM package \"aspnet-prerendering\"'",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Microsoft",
"license": "Apache-2.0",
"dependencies": {
"domain-task": "^1.0.1",
"es6-promise": "^3.1.2"
}
}

View File

@@ -0,0 +1,148 @@
import 'es6-promise';
import * as url from 'url';
import * as path from 'path';
import * as domain from 'domain';
import { run as domainTaskRun } from 'domain-task/main';
import { baseUrl } from 'domain-task/fetch';
export interface RenderToStringCallback {
(error: any, result: RenderToStringResult): void;
}
export interface RenderToStringResult {
html: string;
globals: { [key: string]: any };
}
export interface BootFunc {
(params: BootFuncParams): Promise<RenderToStringResult>;
}
export interface BootFuncParams {
location: url.Url;
url: string;
absoluteUrl: string;
domainTasks: Promise<any>;
}
export interface BootModuleInfo {
moduleName: string;
exportName?: string;
webpackConfig?: string;
}
export function renderToString(callback: RenderToStringCallback, applicationBasePath: string, bootModule: BootModuleInfo, absoluteRequestUrl: string, requestPathAndQuery: string) {
findBootFunc(applicationBasePath, bootModule, (findBootFuncError, bootFunc) => {
if (findBootFuncError) {
callback(findBootFuncError, null);
return;
}
// Prepare a promise that will represent the completion of all domain tasks in this execution context.
// The boot code will wait for this before performing its final render.
let domainTaskCompletionPromiseResolve;
const domainTaskCompletionPromise = new Promise((resolve, reject) => {
domainTaskCompletionPromiseResolve = resolve;
});
const params: BootFuncParams = {
location: url.parse(requestPathAndQuery),
url: requestPathAndQuery,
absoluteUrl: absoluteRequestUrl,
domainTasks: domainTaskCompletionPromise
};
// Open a new domain that can track all the async tasks involved in the app's execution
domainTaskRun(/* code to run */ () => {
// Workaround for Node bug where native Promise continuations lose their domain context
// (https://github.com/nodejs/node-v0.x-archive/issues/8648)
// The domain.active property is set by the domain-context module
bindPromiseContinuationsToDomain(domainTaskCompletionPromise, domain['active']);
// Make the base URL available to the 'domain-tasks/fetch' helper within this execution context
baseUrl(absoluteRequestUrl);
// Actually perform the rendering
bootFunc(params).then(successResult => {
callback(null, { html: successResult.html, globals: successResult.globals });
}, error => {
callback(error, null);
});
}, /* completion callback */ errorOrNothing => {
if (errorOrNothing) {
callback(errorOrNothing, null);
} else {
// There are no more ongoing domain tasks (typically data access operations), so we can resolve
// the domain tasks promise which notifies the boot code that it can do its final render.
domainTaskCompletionPromiseResolve();
}
});
});
}
function findBootModule<T>(applicationBasePath: string, bootModule: BootModuleInfo, callback: (error: any, foundModule: T) => void) {
const bootModuleNameFullPath = path.resolve(applicationBasePath, bootModule.moduleName);
if (bootModule.webpackConfig) {
const webpackConfigFullPath = path.resolve(applicationBasePath, bootModule.webpackConfig);
let aspNetWebpackModule: any;
try {
aspNetWebpackModule = require('aspnet-webpack');
} catch (ex) {
callback('To load your boot module via webpack (i.e., if you specify a \'webpackConfig\' option), you must install the \'aspnet-webpack\' NPM package.', null);
return;
}
aspNetWebpackModule.loadViaWebpack(webpackConfigFullPath, bootModuleNameFullPath, callback);
} else {
callback(null, require(bootModuleNameFullPath));
}
}
function findBootFunc(applicationBasePath: string, bootModule: BootModuleInfo, callback: (error: any, bootFunc: BootFunc) => void) {
// First try to load the module (possibly via Webpack)
findBootModule<any>(applicationBasePath, bootModule, (findBootModuleError, foundBootModule) => {
if (findBootModuleError) {
callback(findBootModuleError, null);
return;
}
// Now try to pick out the function they want us to invoke
let bootFunc: BootFunc;
if (bootModule.exportName) {
// Explicitly-named export
bootFunc = foundBootModule[bootModule.exportName];
} else if (typeof foundBootModule !== 'function') {
// TypeScript-style default export
bootFunc = foundBootModule.default;
} else {
// Native default export
bootFunc = foundBootModule;
}
// Validate the result
if (typeof bootFunc !== 'function') {
if (bootModule.exportName) {
callback(`The module at ${ bootModule.moduleName } has no function export named ${ bootModule.exportName }.`, null);
} else {
callback(`The module at ${ bootModule.moduleName } does not export a default function, and you have not specified which export to invoke.`, null);
}
} else {
callback(null, bootFunc);
}
});
}
function bindPromiseContinuationsToDomain(promise: Promise<any>, domainInstance: domain.Domain) {
const originalThen = promise.then;
promise.then = function then(resolve, reject) {
if (typeof resolve === 'function') {
resolve = domainInstance.bind(resolve);
}
if (typeof reject === 'function') {
reject = domainInstance.bind(reject);
}
return originalThen.call(this, resolve, reject);
};
}

View File

@@ -0,0 +1 @@
export * from './Prerendering';

View File

@@ -0,0 +1,12 @@
{
"compilerOptions": {
"moduleResolution": "node",
"module": "commonjs",
"target": "es5",
"declaration": true,
"outDir": "."
},
"exclude": [
"node_modules"
]
}

View File

@@ -0,0 +1,18 @@
{
"version": "v4",
"repo": "borisyankov/DefinitelyTyped",
"ref": "master",
"path": "typings",
"bundle": "typings/tsd.d.ts",
"installed": {
"node/node.d.ts": {
"commit": "0144ad5a74053f2292424847259c4c8e1d0fecaa"
},
"es6-promise/es6-promise.d.ts": {
"commit": "0144ad5a74053f2292424847259c4c8e1d0fecaa"
},
"whatwg-fetch/whatwg-fetch.d.ts": {
"commit": "dade4414712ce84e3c63393f1aae407e9e7e6af7"
}
}
}