Skip to content

Commit ec9544c

Browse files
Add aspnet-prerendering NPM package
1 parent e5a6a05 commit ec9544c

9 files changed

Lines changed: 220 additions & 0 deletions

File tree

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/typings/
2+
/node_modules/
3+
/*.js
4+
/*.d.ts
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
!/*.js
2+
!/*.d.ts
3+
/typings/
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
Copyright (c) .NET Foundation. All rights reserved.
2+
3+
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
4+
these files except in compliance with the License. You may obtain a copy of the
5+
License at
6+
7+
http://www.apache.org/licenses/LICENSE-2.0
8+
9+
Unless required by applicable law or agreed to in writing, software distributed
10+
under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
11+
CONDITIONS OF ANY KIND, either express or implied. See the License for the
12+
specific language governing permissions and limitations under the License.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Not for general use
2+
3+
This NPM package is an internal implementation detail of the `Microsoft.AspNet.SpaServices` NuGet package.
4+
5+
You should not use this package directly in your own applications, because it is not supported, and there are no
6+
guarantees about how its APIs will change in the future.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"name": "aspnet-prerendering",
3+
"version": "1.0.0",
4+
"description": "Helpers for server-side rendering of JavaScript applications in ASP.NET projects. Works in conjunction with the Microsoft.AspNet.SpaServices NuGet package.",
5+
"main": "index.js",
6+
"scripts": {
7+
"prepublish": "tsd update && tsc && echo 'Finished building NPM package \"aspnet-prerendering\"'",
8+
"test": "echo \"Error: no test specified\" && exit 1"
9+
},
10+
"author": "Microsoft",
11+
"license": "Apache-2.0",
12+
"dependencies": {
13+
"domain-task": "^1.0.1",
14+
"es6-promise": "^3.1.2"
15+
}
16+
}
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import 'es6-promise';
2+
import * as url from 'url';
3+
import * as path from 'path';
4+
import * as domain from 'domain';
5+
import { run as domainTaskRun } from 'domain-task/main';
6+
import { baseUrl } from 'domain-task/fetch';
7+
8+
export interface RenderToStringCallback {
9+
(error: any, result: RenderToStringResult): void;
10+
}
11+
12+
export interface RenderToStringResult {
13+
html: string;
14+
globals: { [key: string]: any };
15+
}
16+
17+
export interface BootFunc {
18+
(params: BootFuncParams): Promise<RenderToStringResult>;
19+
}
20+
21+
export interface BootFuncParams {
22+
location: url.Url;
23+
url: string;
24+
absoluteUrl: string;
25+
domainTasks: Promise<any>;
26+
}
27+
28+
export interface BootModuleInfo {
29+
moduleName: string;
30+
exportName?: string;
31+
webpackConfig?: string;
32+
}
33+
34+
export function renderToString(callback: RenderToStringCallback, applicationBasePath: string, bootModule: BootModuleInfo, absoluteRequestUrl: string, requestPathAndQuery: string) {
35+
findBootFunc(applicationBasePath, bootModule, (findBootFuncError, bootFunc) => {
36+
if (findBootFuncError) {
37+
callback(findBootFuncError, null);
38+
return;
39+
}
40+
41+
// Prepare a promise that will represent the completion of all domain tasks in this execution context.
42+
// The boot code will wait for this before performing its final render.
43+
let domainTaskCompletionPromiseResolve;
44+
const domainTaskCompletionPromise = new Promise((resolve, reject) => {
45+
domainTaskCompletionPromiseResolve = resolve;
46+
});
47+
const params: BootFuncParams = {
48+
location: url.parse(requestPathAndQuery),
49+
url: requestPathAndQuery,
50+
absoluteUrl: absoluteRequestUrl,
51+
domainTasks: domainTaskCompletionPromise
52+
};
53+
54+
// Open a new domain that can track all the async tasks involved in the app's execution
55+
domainTaskRun(/* code to run */ () => {
56+
// Workaround for Node bug where native Promise continuations lose their domain context
57+
// (https://github.com/nodejs/node-v0.x-archive/issues/8648)
58+
// The domain.active property is set by the domain-context module
59+
bindPromiseContinuationsToDomain(domainTaskCompletionPromise, domain['active']);
60+
61+
// Make the base URL available to the 'domain-tasks/fetch' helper within this execution context
62+
baseUrl(absoluteRequestUrl);
63+
64+
// Actually perform the rendering
65+
bootFunc(params).then(successResult => {
66+
callback(null, { html: successResult.html, globals: successResult.globals });
67+
}, error => {
68+
callback(error, null);
69+
});
70+
}, /* completion callback */ errorOrNothing => {
71+
if (errorOrNothing) {
72+
callback(errorOrNothing, null);
73+
} else {
74+
// There are no more ongoing domain tasks (typically data access operations), so we can resolve
75+
// the domain tasks promise which notifies the boot code that it can do its final render.
76+
domainTaskCompletionPromiseResolve();
77+
}
78+
});
79+
});
80+
}
81+
82+
function findBootModule<T>(applicationBasePath: string, bootModule: BootModuleInfo, callback: (error: any, foundModule: T) => void) {
83+
const bootModuleNameFullPath = path.resolve(applicationBasePath, bootModule.moduleName);
84+
if (bootModule.webpackConfig) {
85+
const webpackConfigFullPath = path.resolve(applicationBasePath, bootModule.webpackConfig);
86+
87+
let aspNetWebpackModule: any;
88+
try {
89+
aspNetWebpackModule = require('aspnet-webpack');
90+
} catch (ex) {
91+
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);
92+
return;
93+
}
94+
95+
aspNetWebpackModule.loadViaWebpack(webpackConfigFullPath, bootModuleNameFullPath, callback);
96+
} else {
97+
callback(null, require(bootModuleNameFullPath));
98+
}
99+
}
100+
101+
function findBootFunc(applicationBasePath: string, bootModule: BootModuleInfo, callback: (error: any, bootFunc: BootFunc) => void) {
102+
// First try to load the module (possibly via Webpack)
103+
findBootModule<any>(applicationBasePath, bootModule, (findBootModuleError, foundBootModule) => {
104+
if (findBootModuleError) {
105+
callback(findBootModuleError, null);
106+
return;
107+
}
108+
109+
// Now try to pick out the function they want us to invoke
110+
let bootFunc: BootFunc;
111+
if (bootModule.exportName) {
112+
// Explicitly-named export
113+
bootFunc = foundBootModule[bootModule.exportName];
114+
} else if (typeof foundBootModule !== 'function') {
115+
// TypeScript-style default export
116+
bootFunc = foundBootModule.default;
117+
} else {
118+
// Native default export
119+
bootFunc = foundBootModule;
120+
}
121+
122+
// Validate the result
123+
if (typeof bootFunc !== 'function') {
124+
if (bootModule.exportName) {
125+
callback(`The module at ${ bootModule.moduleName } has no function export named ${ bootModule.exportName }.`, null);
126+
} else {
127+
callback(`The module at ${ bootModule.moduleName } does not export a default function, and you have not specified which export to invoke.`, null);
128+
}
129+
} else {
130+
callback(null, bootFunc);
131+
}
132+
});
133+
}
134+
135+
function bindPromiseContinuationsToDomain(promise: Promise<any>, domainInstance: domain.Domain) {
136+
const originalThen = promise.then;
137+
promise.then = function then(resolve, reject) {
138+
if (typeof resolve === 'function') {
139+
resolve = domainInstance.bind(resolve);
140+
}
141+
142+
if (typeof reject === 'function') {
143+
reject = domainInstance.bind(reject);
144+
}
145+
146+
return originalThen.call(this, resolve, reject);
147+
};
148+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './Prerendering';
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"compilerOptions": {
3+
"moduleResolution": "node",
4+
"module": "commonjs",
5+
"target": "es5",
6+
"declaration": true,
7+
"outDir": "."
8+
},
9+
"exclude": [
10+
"node_modules"
11+
]
12+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"version": "v4",
3+
"repo": "borisyankov/DefinitelyTyped",
4+
"ref": "master",
5+
"path": "typings",
6+
"bundle": "typings/tsd.d.ts",
7+
"installed": {
8+
"node/node.d.ts": {
9+
"commit": "0144ad5a74053f2292424847259c4c8e1d0fecaa"
10+
},
11+
"es6-promise/es6-promise.d.ts": {
12+
"commit": "0144ad5a74053f2292424847259c4c8e1d0fecaa"
13+
},
14+
"whatwg-fetch/whatwg-fetch.d.ts": {
15+
"commit": "dade4414712ce84e3c63393f1aae407e9e7e6af7"
16+
}
17+
}
18+
}

0 commit comments

Comments
 (0)